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 188Rust 55Shell 30Markdown 281211 Sutichaykuna

⎇ fe155cf20db4e443ec1cc2fb06924be137aa0cd2 docs/adr/0015-imagenes-ajenas.md Markdown · 375 siq'ikuna · 21.5 KB Ñawpa willay ch'uya

qorpa pull/list: la imagen ajena entra verificada, o no entra fe155cf20 · Sergio ·

1# ADR 0015 — qorpa: el mundo glibc entra enjaulado, y no entra al store
2
3- **Estado:** PROPUESTO — nombre ADOPTADO (`qorpa`, 2026-09-03); implementación EN CURSO por el
4  §Orden de trabajo. Este documento decide la frontera; el código viene después.
5- **Fecha:** 2026-09-03
6- **Frontera (a crear):** `hammer qorpa {pull,create,run,export,list,prune}`,
7  `/var/lib/hammer/qorpa/imagenes/<sha256>/`, `/var/lib/hammer/qorpa/instancias/<id>/`,
8  `instancia.toml` (manifiesto), clase de nodo `ajeno` en `build-state.py`.
9- **Superficie:** verbos en inglés, mensajes en castellano — `CLAUDE.md` regla 4. Este ADR nació
10  proponiendo `traer/crear/correr` y se corrigió el 2026-09-03; el nombre `qorpa` sí es quechua.
11- **Continúa:** [SDD 04](../04-overlay.md) (overlay), [SDD 16](../16-harkaq-jaula.md) (harkaq),
12  [ADR 0004](0004-no-custom-nix.md) (hammer no usa nix), [SDD 20](../20-catalogo-publicable-y-completa.md)
13  (catálogo publicable).
14- **Absorbe:** [`plan-jaula-juegos.md`](../plan-jaula-juegos.md) §Capa 3. Su **F1** pasa a ser un
15  caso particular de este ADR (imagen de tipo 2); su **F0** (flatpak al catálogo) queda innecesaria;
16  su **F2** (glibc + multilib 32-bit desde fuente, «una campaña entera») queda **cancelada**.
17
18---
19
20## Contexto
21
22### El problema, medido
23
24El corpus tiene hoy 788 recetas selladas y cuatro escritorios que cierran. De apps de usuario final
25tiene esto:
26
27```
28kate konsole dolphin okular gwenview ark spectacle kcalc filelight kfind · foot · vim
29```
30
31Ni navegador, ni suite ofimática, ni reproductor, ni editor de imagen. La pregunta natural es qué
32lo impide, y la respuesta se parte en tres montones **con causas distintas**:
33
34| montón | qué hay adentro | qué lo bloquea |
35|---|---|---|
36| **A** | Firefox, Chromium, LibreOffice, mpv, GIMP, Inkscape, Blender, OBS | **nada estructural**: son Wayland-nativos y compilables. Falta escribirlas. |
37| **B** | Steam/Proton, IDEs de JetBrains, Zoom/Slack/Discord/Spotify, binarios comerciales | **la libc**. Son prebuilts glibc. Mueren en el loader, antes de tocar el servidor gráfico. |
38| **C** | herramental X11: `xdotool`, `xbindkeys`, `x11vnc`, Barrier | **Wayland**, y Xwayland tampoco los salva (sólo ve clientes X). |
39
40La medición que ordena el debate: **la intersección de «sólo X11» con «compilable desde fuente en
41musl» es casi vacía**. Casi todo lo que se pierde por Wayland ya estaba perdido por musl. El montón
42que duele es **B**, y ninguna cantidad de trabajo en el corpus lo mueve: son binarios que nadie va a
43recompilar.
44
45### Por qué no alcanza con lo que ya decidimos
46
47Tres salidas parecen razonables y las tres son peores:
48
491. **Construir glibc + multilib 32-bit en el corpus.** Es la F2 del plan de juegos, descrita ahí
50   mismo como «una campaña entera»: toolchain dual, loader, locales. Y aun así no corrés Steam: el
51   cliente espera un userland glibc completo, no una libc suelta.
522. **Flatpak al catálogo** (F0 del mismo plan). Funciona, pero mete ostree + flatpak + su modelo de
53   runtimes y portales, y la cadena de custodia pasa a ser de flathub, no nuestra. Era la validación
54   barata mientras no hubiera jaula propia.
553. **Aflojar Wayland-only.** No resuelve B —el problema es la libc— y sí revierte una decisión
56   tomada con fundamento.
57
58### Lo que ya está construido
59
60Este ADR compone piezas existentes. No inventa infraestructura:
61
62| pieza | dónde | estado |
63|---|---|---|
64| aislamiento de namespaces | `recipes/bwrap.toml` | sellada; ya es dep del lab de build |
65| capa mutable sobre base inmutable | [SDD 04](../04-overlay.md) + `crates/hammer-overlay` | overlayfs, `hammer try` |
66| política de grano fino en el kernel | [SDD 16](../16-harkaq-jaula.md) | Landlock + seccomp; fase 1 ✅, barrido con 0 irreducibles |
67| rootfs ajeno pineado por sha256 | `scripts/lab-image.sh` | en producción desde 2026-08-10 |
68| `newuidmap` / `newgidmap` / `/etc/subuid` | `recipes/shadow.toml` | **instalados**; falta provisionarlos |
69| kernel con lo necesario | `recipes/linux-metal.toml:111-114` | `OVERLAY_FS` (+redirect_dir/index/xino/metacopy), `USER_NS`, `NAMESPACES`, `SECURITY_LANDLOCK`, `SECCOMP` |
70
71Nada de esto hay que re-sellar. El kernel propio ya arranca con todo encendido.
72
73---
74
75## Decisión
76
77**Se adopta un subsistema de imágenes ajenas: rootfs de otras distribuciones, traídos enteros y
78pineados por digest, que corren enjaulados sobre el mismo kernel y comparten con el escritorio
79únicamente protocolos y nodos de dispositivo. Sus artefactos NO entran al store.**
80
81### D1 — La invariante del store
82
83> **Una imagen ajena no es un artefacto y no vive en el store.**
84
85No es prolijidad. El store promete que **cada entrada se reconstruye desde fuente bit a bit**. Un
86rootfs de Fedora no. Meterlo ahí haría que el store mienta, y sería la misma clase de error que el
87artefacto vacío que ya nos costó caro: *algo que se lee como garantía y no lo es*
88(`CLAUDE.md` regla 3 — «un ausente falla ruidosamente; un vacío llega hasta el final diciendo que
89todo fue bien»).
90
91Espacio de nombres paralelo, direccionado por digest, **fuera de `hash_inputs` de todo**:
92
93```
94/var/lib/hammer/qorpa/imagenes/<sha256>/  imagen  — INMUTABLE, verificada por digest
95/var/lib/hammer/qorpa/instancias/<id>/
96    ├── instancia.toml                    manifiesto — LA VERDAD
97    ├── upper/                            capa mutable — CACHÉ
98    └── work/                             overlayfs
99```
100
101El precedente exacto ya existe y funciona: `scripts/lab-image.sh` trae el rootfs del lab pineado por
102sha256 porque `apk add` contra Alpine edge es irrepetible por diseño. Misma figura, otro consumidor.
103
104### D2 — La regla del borde
105
106> **Se cruza el borde con protocolos y nodos de dispositivo, nunca con librerías compartidas.**
107
108| qué | cómo cruza | por qué |
109|---|---|---|
110| pantalla | socket `wayland-0` | protocolo, no ABI. La imagen trae su `libwayland`. |
111| audio | socket de PipeWire | ídem |
112| IPC | socket de D-Bus (opcional, por sesión) | ídem |
113| GPU | `/dev/dri/*` | ABI de kernel (DRM), estable |
114| juegos | `/dev/ntsync` | ABI de kernel |
115| input | `/dev/input/*` (sólo si se concede) | ABI de kernel |
116| **Mesa / libc / cualquier `.so`** | **no cruza** | nuestro Mesa es musl; un proceso glibc no lo carga. La imagen trae el suyo, que habla DRM con nuestro kernel sin conocerlo. |
117
118El corolario práctico: **la jaula no sabe ni le importa qué libc hay adentro.** Glibc, musl ajeno,
119uclibc, bionic. Lo único que cruza es la ABI del kernel, que es estable por contrato. Por eso este
120mecanismo resuelve el montón B completo de una sola vez.
121
122El error a evitar —y es el que convierte esto en un pantano— es intentar compartir librerías del
123host «para ahorrar espacio».
124
125### D3 — El manifiesto es la verdad; el `upper` es caché
126
127> **Una instancia se declara, no se acumula.**
128
129Si el `upper` con 200 paquetes instalados a mano fuese el activo, tendríamos un blob irreemplazable:
130justo lo que hammer existe para no tener. La relación correcta es la misma que receta↔artefacto:
131
132```toml
133# /var/lib/hammer/qorpa/instancias/juegos/instancia.toml
134base    = "sha256:…"                  # digest de la imagen, inmutable
135distro  = "fedora-43"                 # informativo
136paquetes = ["steam", "mesa-dri-drivers", "mesa-vulkan-drivers"]
137
138[concesiones]                          # POR DEFECTO NADA
139sockets    = ["wayland", "pipewire"]
140devices    = ["/dev/dri", "/dev/ntsync"]
141directorios = [{ host = "~/Juegos", dentro = "/home/usuario/Juegos", modo = "rw" }]
142red        = true                      # lo necesita dnf y lo necesita Steam
143
144[exporta]                              # capa de transparencia; ver D5
145binarios = ["steam"]
146apps     = ["steam.desktop"]
147```
148
149Consecuencias que se caen solas:
150- **`hammer qorpa recreate <id>`** reconstruye la instancia desde el manifiesto. El `upper` es
151  descartable.
152- **Actualizar la base** (Fedora 43 → 44) no es un rebase riesgoso: se cambia el digest y se recrea.
153- **El respaldo** es el manifiesto (KB), no el `upper` (GB). `respaldo-storagebox.sh` no toca
154  `/var/lib/hammer/qorpa/instancias/*/upper` y eso es correcto, no un olvido.
155- La poda tiene una regla trivial: **un `upper` siempre se puede borrar.**
156
157### D4 — Granularidad: cuatro tipos, no uno
158
159| tipo | qué es | mutable | ¿sellable? | para qué |
160|---|---|---|---|---|
161| **1** | rootfs completo con gestor (Fedora+`dnf`, Arch+`pacman`) | sí | no | «literalmente lo que sea» |
162| **2** | runtime curado congelado (Steam Linux Runtime *sniper*, runtime freedesktop) | **no** | **sí**, `file_drop` por sha256 | cadena de custodia nuestra |
163| **3** | bundle por app (AppImage) | no | sí | una app suelta |
164| **4** | glibc en un prefijo, sin jaula | — | — | **rechazado**: frágil y ensucia justo lo que se protege |
165
166El tipo 2 es la F1 del plan de juegos y **sigue siendo el preferido cuando alcanza**: es inmutable,
167se sella y su digest entra en el índice firmado. El tipo 1 existe porque `dnf install` es
168precisamente lo que el tipo 2 no permite.
169
170### D5 — Transparencia: shims generados, no un sistema de ficheros
171
172El objetivo es el poder de Bedrock Linux —un espacio de nombres unificado, apps de cualquier
173«stratum» disponibles en todos lados— **sin sus formas**: un FUSE global en el camino de cada
174`exec`, integración a nivel de PID 1, y arbitraje por heurística de qué binario gana.
175
176Acá no hace falta nada de eso, porque **el FHS de esta distro ya es una proyección, no la verdad**:
177`hammer hydrate` proyecta artefactos del store a un árbol. Extender la proyección a instancias es
178idiomático. Tres capas:
179
180**Capa 1 — shims.** Al crear o actualizar una instancia se **generan** lanzadores finos en el
181espacio del host: un script que hace `exec` hacia adentro, más el `.desktop` con su icono. Firefox
182de Fedora aparece en el menú de Plasma al lado de kate; `steam` está en el `PATH`.
183
184Gana en tres cosas concretas contra el FUSE:
185- **cero costo en runtime** — no hay proceso en el camino crítico de cada `exec`;
186- **inspeccionable** — `cat` al shim y ves exactamente qué hace;
187- **revocable** — se borran los shims y la instancia desaparece del sistema sin desmontar nada.
188
189⚠ **Se generan, no se copian.** El `Exec=` de un `.desktop` ajeno es texto ajeno; copiarlo pondría
190una línea de comando que no escribimos en el menú del usuario.
191
192**Capa 2 — se exporta lo declarado.** Si se exporta todo, el `ls` y el `systemctl` de Fedora compiten
193con los nuestros. Ésa es la falla de Bedrock: **arbitra en tiempo de `exec`**, con heurísticas. Acá
194la lista `[exporta]` del manifiesto resuelve la ambigüedad **al declarar**. Tiene precedente directo:
195la hidratación del escritorio ya va por lista explícita (`TARGETS="…"`), nunca por glob.
196
197**Capa 3 — el grafo lo sabe.** Los nodos exportados entran en `build-state.json` con **clase
198`ajeno`**, y el reporte pasa a decir:
199
200```
201escritorio-kde   171 nativas + 6 ajenas    (ajenas: sin procedencia de fuente, sin repro)
202```
203
204Bedrock no puede decirte qué tenés. Nosotros sí, y esto cierra el riesgo real de todo el ADR: que en
205seis meses alguien cuente esas apps como parte del corpus. **En otra clase no se pueden contar mal.**
206
207### D6 — El caso Steam, que es el que ordena el diseño
208
209Steam **ya es un contenedor**: el cliente corre en el sistema y los juegos corren dentro de
210*pressure-vessel*, el contenedor bwrap de Valve con el runtime *sniper*. La forma correcta no es
211meter Steam en nuestra jaula sino anidarlas:
212
213```
214arje-zero → kwin (wayland)
215  └─ instancia glibc (tipo 1: drivers, libs de 32 bits, dnf)
216       └─ cliente Steam
217            └─ pressure-vessel (tipo 2, de Valve)      ← anidado
218                 └─ el juego + Proton
219```
220
221Corre mejor así porque **es la configuración exacta que Valve prueba**. Nosotros ponemos el nivel de
222afuera y la política; adentro no tocamos nada.
223
224⚠ **Verificar, no asumir:** pressure-vessel necesita crear user namespaces *desde dentro* de uno ya
225creado. El kernel los tiene encendidos, pero el anidamiento hay que probarlo con un juego real. Es
226la clase de cosa que sella verde y falla en la mano del usuario.
227
228### D7 — La política se escribe, y es la única vez
229
230harkaq deriva la política en vez de escribirla: `política = clausura(deps declaradas)`, y de ahí sale
231la evidencia negativa que hace valioso al subproducto ([SDD 16](../16-harkaq-jaula.md) §3).
232
233**Una imagen ajena no tiene clausura declarada.** Es el primer y único lugar del sistema donde la
234política hay que **autorarla**. No es un defecto: es una excepción que conviene nombrar y acotar en
235vez de descubrir. El bloque `[concesiones]` del manifiesto es esa política, **por defecto vacío**, y
236se compila al mismo `PolicySpec` que ya consume `harkaq-exec`.
237
238Un `dnf` que no alcanza `~/.ssh` es un argumento que ninguna distro ofrece.
239
240### D8 — Qué se pinea (el suelo, no la instancia) y cuántas imágenes se curan
241
242Dos preguntas que se confunden y tienen respuestas distintas.
243
244**a) Pinear no congela: fija el punto de partida.** El digest ES la identidad, igual que
245`alpine-minirootfs`, `zig` y el rootfs del lab. Lo que queda dentro y fuera del pin:
246
247| qué | ¿pineado? | cómo se actualiza |
248|---|---|---|
249| el rootfs base | **sí**, por sha256 | cambiar el digest en `instancia.toml` + `hammer qorpa recreate` |
250| lo que instalás adentro (`dnf install steam`, `pacman -Syu`) | **no, y no puede estarlo** | con el gestor de la imagen, cuando quieras |
251
252Subir la base de versión es barato **precisamente por D3**: como el manifiesto es la verdad y el
253`upper/` es caché, se cambia `base = "sha256:…"` y se recrea. Sin D3 sería un rebase sobre un blob
254irreemplazable; con D3 son dos líneas y un rato de red.
255
256El riesgo real del pin —que upstream borre el tarball— **ya está resuelto por
257[ADR 0013](0013-mirror-de-fuentes.md)**: la URL no entra en la identidad, sólo el sha256, así que
258espejar una imagen es gratis y una imagen pineada no se puede perder.
259
260**b) Pinear ≠ lista cerrada.** `hammer qorpa pull <url> --sha256` acepta cualquier rootfs. Lo corto
261no es lo que *se puede* traer, sino lo que **nosotros probamos, espejamos y publicamos**, porque cada
262imagen curada es una segunda cadena de suministro que hay que sostener (§NO-resuelve 3). Se curan
263**tres**, y cada una entra por un trabajo distinto — no por sabor:
264
265| imagen | tipo | qué trabajo hace | pin (verificado 2026-09-03) |
266|---|---|---|---|
267| **Arch bootstrap** | 1 | juegos: `multilib` de 32 bits de primera clase, y SteamOS *es* Arch ⇒ extiende el argumento de D6 («la configuración exacta que Valve prueba») | `archlinux-bootstrap-2026.09.01-x86_64.tar.zst`, sha256 `895661bd…`, en `archive.archlinux.org` (archivado para siempre) |
268| **Ubuntu base LTS** | 1 | binarios comerciales: Zoom, Slack, Discord, Spotify, Chrome, JetBrains se compilan y prueban contra esto | `ubuntu-base-24.04.3-base-amd64.tar.gz`, sha256 `6bc2cde3…`, `cdimage.ubuntu.com` + `old-releases` |
269| **Steam Runtime «sniper»** | 2 | la única **sellable**: al no mutar entra al store por `file_drop` y su digest va al índice firmado — cadena de custodia nuestra | ⚠ pin sin verificar todavía |
270
271**Fedora queda BYO, no curada.** No por calidad —publica `Fedora-Container-Base-Generic-43-1.6`
272con CHECKSUM firmado y se pinea igual de bien— sino porque hace el **mismo trabajo que Arch** en el
273slot «fresco», y una tercera cadena mutable es justo la normalización que §NO-resuelve 3 quiere
274evitar. El ejemplo de D3 sigue diciendo `fedora-43` a propósito: el mecanismo no distingue, y eso es
275la demostración de que la terna es una decisión de curaduría, no del diseño.
276
277**c) Compartibles, más adelante.** Una imagen pineada es cuerpo inmutable direccionado por
278contenido, así que **[ADR 0014](0014-distribucion-multiorigen.md) se le aplica sin cambiar nada**:
279puede servirse desde cualquier origen porque el digest la verifica, y ausencia ⇒ seguir mientras que
280contenido distinto ⇒ ABORTAR. No se hace el día uno, pero no hay que diseñar nada para que se pueda:
281lo único que falta es la decisión. ⚠ Antes de publicarlas a terceros (no a nuestras propias
282máquinas, que es lo que ya hace ADR 0013 con las fuentes) hay que mirar licencia y **marca**:
283redistribuir un rootfs de Ubuntu o Fedora sin modificar suele estar permitido, pero la política de
284marca es cosa aparte y no es una pregunta técnica.
285
286---
287
288## Lo que este ADR admite que NO resuelve
289
290Se escriben acá para que no se descubran en producción.
291
2921. **El socket de Wayland es un borde de privilegio, no un caño.** Pasarlo crudo le da a la imagen
293   ajena todos los protocolos privilegiados que exponga el compositor: `screencopy`,
294   `virtual-keyboard`, `input-method`. Flatpak no pasa el socket: pasa un **proxy que filtra
295   globals**. No lo tenemos. Mientras no exista, **una instancia con socket de Wayland puede
296   capturar la pantalla y sintetizar teclas**, y el manifiesto debe decirlo en la cara del usuario.
297   Cerrarlo es tarea propia y probablemente el ticket más valioso que sale de este ADR.
298
2992. **UID mapping.** Sin userns privilegiados, `root` adentro es tu UID afuera — que es la propiedad
300   de seguridad que queremos, pero rompe operaciones que hacen `chown` a UIDs distintos (rpm crea
301   usuarios de servicio). La salida son rangos subuid, y **las piezas ya están en el corpus**:
302   `recipes/shadow.toml` instala `newuidmap`/`newgidmap` y crea `/etc/subuid` vacío. Falta
303   provisionarlo (capacidad de fichero o setuid + rango en `/etc/subuid`) y probarlo con un `dnf
304   install` real. Es la primera cosa que va a fallar.
305
3063. **Segunda cadena de suministro, sin ninguna garantía de hammer.** Sin repro, sin cierre firmado,
307   sin escaneo de licencias. El riesgo no es técnico: es que se normalice. Mitigación = D5 capa 3
308   (clase `ajeno` en el grafo) + un renglón explícito en [SDD 20](../20-catalogo-publicable-y-completa.md):
309   **las imágenes ajenas no entran en el catálogo publicable ni en el reporte de licencias, porque
310   no podemos enumerarlas.**
311
3124. **El alcance del claim.** Desde el día que exista una instancia, la frase «hammer reproduce bit a
313   bit» hay que acotarla por escrito: *el sistema base reproduce; las instancias ajenas no, y se
314   declaran como tales*. Sin eso, la cultura de números honestos se erosiona sola — que es
315   exactamente la falla que ya nos pasó con el store-gc y con la deuda fantasma.
316
3175. **Disco.** Un rootfs de Fedora son 1-2 G y no lo alcanzan `store-gc` ni `.dmerge`. Necesita poda
318   propia. Este repo ya tuvo tres emergencias de disco; que la poda nazca con el subsistema, no
319   después.
320
3216. **Red adentro.** `dnf` la necesita. Es la concesión más peligrosa del manifiesto y por eso es la
322   única que se escribe sola en una línea aparte. También hace falta `/etc/resolv.conf`: es el
323   segundo fallo clásico de la primera corrida.
324
3257. **X entre instancias.** Un Xwayland dentro de una instancia levanta *su* servidor X. Portapapeles
326   con el escritorio funciona (pasa por el compositor); entre dos instancias, no. Aceptado.
327
328---
329
330## Lo que se cae, y lo que se resuelve gratis
331
332- **`xwayland` deja de ser deuda del corpus.** Si las apps X sólo corren dentro de instancias, el
333  Xwayland va **adentro de la imagen** —Fedora y Arch ya lo traen— y se cuelga de nuestro kwin por
334  el socket. El nodo `wanted` de `escritorio-kde` se disuelve sin escribir la receta y sin tocar la
335  postura Wayland-only. `targets.toml` debe sacarlo de las raíces con una nota que apunte acá.
336- **GIMP e Inkscape dejan de reabrir GTK3.** Son GTK3 y GTK3 está aparcado a propósito
337  (ver la memoria del frente GNOME). Con instancias, corren sin traer el árbol GTK3 al corpus.
338- **F2 del plan de juegos, cancelada.** Las libs de 32 bits las trae la imagen.
339- **F0 del plan de juegos, innecesaria.** No hace falta meter flatpak+ostree al catálogo para tener
340  juegos.
341
342## Lo que este ADR NO cambia
343
344- **El montón A se sigue construyendo nativo.** La jaula es para binarios ajenos, no una excusa para
345  no empaquetar. Orden acordado: **mpv → OBS → Firefox**.
346- **Wayland-only sigue en pie**, y este ADR es lo que lo hace barato.
347- **arje-zero sigue siendo PID 1.** Nada de integración a nivel de init, que es una de las formas de
348  Bedrock que se rechazan explícitamente.
349- **Cada instancia se gestiona con su propio gestor**, entrando explícitamente. Nada de multiplexar
350  `dnf`/`pacman` detrás de un comando único (el `pmm` de Bedrock): es menos mágico y no miente.
351
352---
353
354## Orden de trabajo propuesto
355
3561. **Provisionar subuid** y probar `dnf install` en una instancia mínima. Es lo primero que falla
357   (§NO-resuelve 2) y define si el resto es fácil o difícil.
3582. `hammer qorpa pull <url> --sha256` + verificación de digest. Reusa el patrón de `lab-image.sh`.
3593. Instancia = overlay sobre la imagen + `instancia.toml`; `create` / `recreate` / `run`.
3604. Concesiones → `PolicySpec` de harkaq. Empezar por **nada** y abrir sólo lo declarado.
3615. Shims + `.desktop` generados (`export`), y clase `ajeno` en `build-state.py`.
3626. **Steam de punta a punta**, con verificación explícita del bwrap anidado.
3637. Poda (`hammer qorpa prune`) y renglón en SDD 20 sobre licencias.
3648. Proxy filtrante de Wayland — ticket propio, el más valioso de la lista.
365
366## Nombre — ADOPTADO 2026-09-03
367
368El subsistema se llama **`qorpa`** — *huésped alojado*: alguien que entra a la casa, se le dan
369habitaciones concretas y no las demás. Elegido por el autor del repo; entra en la familia (harkaq,
370yupana, arje, wawafs, kikin, churay, mirada, minga).
371
372Consecuencias de nombre, ya aplicadas arriba: la frontera es `hammer qorpa {…}`, el espacio de
373nombres es `/var/lib/hammer/qorpa/{imagenes,instancias}/` —uno solo, para que la poda de
374§NO-resuelve 5 tenga un único árbol que barrer— y el adjetivo del grafo sigue siendo `ajeno`
375(clase de nodo), porque describe la **procedencia** del nodo, no el subsistema que lo aloja.

0015-imagenes-ajenas.md

ADR 0015 — qorpa: el mundo glibc entra enjaulado, y no entra al store


Contexto

El problema, medido

El corpus tiene hoy 788 recetas selladas y cuatro escritorios que cierran. De apps de usuario final tiene esto:

kate konsole dolphin okular gwenview ark spectacle kcalc filelight kfind · foot · vim

Ni navegador, ni suite ofimática, ni reproductor, ni editor de imagen. La pregunta natural es qué lo impide, y la respuesta se parte en tres montones con causas distintas:

montónqué hay adentroqué lo bloquea
AFirefox, Chromium, LibreOffice, mpv, GIMP, Inkscape, Blender, OBSnada estructural: son Wayland-nativos y compilables. Falta escribirlas.
BSteam/Proton, IDEs de JetBrains, Zoom/Slack/Discord/Spotify, binarios comercialesla libc. Son prebuilts glibc. Mueren en el loader, antes de tocar el servidor gráfico.
Cherramental X11: xdotool, xbindkeys, x11vnc, BarrierWayland, y Xwayland tampoco los salva (sólo ve clientes X).

La medición que ordena el debate: la intersección de «sólo X11» con «compilable desde fuente en musl» es casi vacía. Casi todo lo que se pierde por Wayland ya estaba perdido por musl. El montón que duele es B, y ninguna cantidad de trabajo en el corpus lo mueve: son binarios que nadie va a recompilar.

Por qué no alcanza con lo que ya decidimos

Tres salidas parecen razonables y las tres son peores:

  1. Construir glibc + multilib 32-bit en el corpus. Es la F2 del plan de juegos, descrita ahí mismo como «una campaña entera»: toolchain dual, loader, locales. Y aun así no corrés Steam: el cliente espera un userland glibc completo, no una libc suelta.
  2. Flatpak al catálogo (F0 del mismo plan). Funciona, pero mete ostree + flatpak + su modelo de runtimes y portales, y la cadena de custodia pasa a ser de flathub, no nuestra. Era la validación barata mientras no hubiera jaula propia.
  3. Aflojar Wayland-only. No resuelve B —el problema es la libc— y sí revierte una decisión tomada con fundamento.

Lo que ya está construido

Este ADR compone piezas existentes. No inventa infraestructura:

piezadóndeestado
aislamiento de namespacesrecipes/bwrap.tomlsellada; ya es dep del lab de build
capa mutable sobre base inmutableSDD 04 + crates/hammer-overlayoverlayfs, hammer try
política de grano fino en el kernelSDD 16Landlock + seccomp; fase 1 ✅, barrido con 0 irreducibles
rootfs ajeno pineado por sha256scripts/lab-image.shen producción desde 2026-08-10
newuidmap / newgidmap / /etc/subuidrecipes/shadow.tomlinstalados; falta provisionarlos
kernel con lo necesariorecipes/linux-metal.toml:111-114OVERLAY_FS (+redirect_dir/index/xino/metacopy), USER_NS, NAMESPACES, SECURITY_LANDLOCK, SECCOMP

Nada de esto hay que re-sellar. El kernel propio ya arranca con todo encendido.


Decisión

Se adopta un subsistema de imágenes ajenas: rootfs de otras distribuciones, traídos enteros y pineados por digest, que corren enjaulados sobre el mismo kernel y comparten con el escritorio únicamente protocolos y nodos de dispositivo. Sus artefactos NO entran al store.

D1 — La invariante del store

Una imagen ajena no es un artefacto y no vive en el store.

No es prolijidad. El store promete que cada entrada se reconstruye desde fuente bit a bit. Un rootfs de Fedora no. Meterlo ahí haría que el store mienta, y sería la misma clase de error que el artefacto vacío que ya nos costó caro: algo que se lee como garantía y no lo es (CLAUDE.md regla 3 — «un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien»).

Espacio de nombres paralelo, direccionado por digest, fuera de hash_inputs de todo:

/var/lib/hammer/qorpa/imagenes/<sha256>/  imagen  — INMUTABLE, verificada por digest
/var/lib/hammer/qorpa/instancias/<id>/
    ├── instancia.toml                    manifiesto — LA VERDAD
    ├── upper/                            capa mutable — CACHÉ
    └── work/                             overlayfs

El precedente exacto ya existe y funciona: scripts/lab-image.sh trae el rootfs del lab pineado por sha256 porque apk add contra Alpine edge es irrepetible por diseño. Misma figura, otro consumidor.

D2 — La regla del borde

Se cruza el borde con protocolos y nodos de dispositivo, nunca con librerías compartidas.

quécómo cruzapor qué
pantallasocket wayland-0protocolo, no ABI. La imagen trae su libwayland.
audiosocket de PipeWireídem
IPCsocket de D-Bus (opcional, por sesión)ídem
GPU/dev/dri/*ABI de kernel (DRM), estable
juegos/dev/ntsyncABI de kernel
input/dev/input/* (sólo si se concede)ABI de kernel
Mesa / libc / cualquier .sono cruzanuestro Mesa es musl; un proceso glibc no lo carga. La imagen trae el suyo, que habla DRM con nuestro kernel sin conocerlo.

El corolario práctico: la jaula no sabe ni le importa qué libc hay adentro. Glibc, musl ajeno, uclibc, bionic. Lo único que cruza es la ABI del kernel, que es estable por contrato. Por eso este mecanismo resuelve el montón B completo de una sola vez.

El error a evitar —y es el que convierte esto en un pantano— es intentar compartir librerías del host «para ahorrar espacio».

D3 — El manifiesto es la verdad; el upper es caché

Una instancia se declara, no se acumula.

Si el upper con 200 paquetes instalados a mano fuese el activo, tendríamos un blob irreemplazable: justo lo que hammer existe para no tener. La relación correcta es la misma que receta↔artefacto:

# /var/lib/hammer/qorpa/instancias/juegos/instancia.toml
base    = "sha256:…"                  # digest de la imagen, inmutable
distro  = "fedora-43"                 # informativo
paquetes = ["steam", "mesa-dri-drivers", "mesa-vulkan-drivers"]

[concesiones]                          # POR DEFECTO NADA
sockets    = ["wayland", "pipewire"]
devices    = ["/dev/dri", "/dev/ntsync"]
directorios = [{ host = "~/Juegos", dentro = "/home/usuario/Juegos", modo = "rw" }]
red        = true                      # lo necesita dnf y lo necesita Steam

[exporta]                              # capa de transparencia; ver D5
binarios = ["steam"]
apps     = ["steam.desktop"]

Consecuencias que se caen solas:

D4 — Granularidad: cuatro tipos, no uno

tipoqué esmutable¿sellable?para qué
1rootfs completo con gestor (Fedora+dnf, Arch+pacman)síno«literalmente lo que sea»
2runtime curado congelado (Steam Linux Runtime sniper, runtime freedesktop)nosí, file_drop por sha256cadena de custodia nuestra
3bundle por app (AppImage)nosíuna app suelta
4glibc en un prefijo, sin jaula——rechazado: frágil y ensucia justo lo que se protege

El tipo 2 es la F1 del plan de juegos y sigue siendo el preferido cuando alcanza: es inmutable, se sella y su digest entra en el índice firmado. El tipo 1 existe porque dnf install es precisamente lo que el tipo 2 no permite.

D5 — Transparencia: shims generados, no un sistema de ficheros

El objetivo es el poder de Bedrock Linux —un espacio de nombres unificado, apps de cualquier «stratum» disponibles en todos lados— sin sus formas: un FUSE global en el camino de cada exec, integración a nivel de PID 1, y arbitraje por heurística de qué binario gana.

Acá no hace falta nada de eso, porque el FHS de esta distro ya es una proyección, no la verdad: hammer hydrate proyecta artefactos del store a un árbol. Extender la proyección a instancias es idiomático. Tres capas:

Capa 1 — shims. Al crear o actualizar una instancia se generan lanzadores finos en el espacio del host: un script que hace exec hacia adentro, más el .desktop con su icono. Firefox de Fedora aparece en el menú de Plasma al lado de kate; steam está en el PATH.

Gana en tres cosas concretas contra el FUSE:

⚠ Se generan, no se copian. El Exec= de un .desktop ajeno es texto ajeno; copiarlo pondría una línea de comando que no escribimos en el menú del usuario.

Capa 2 — se exporta lo declarado. Si se exporta todo, el ls y el systemctl de Fedora compiten con los nuestros. Ésa es la falla de Bedrock: arbitra en tiempo de exec, con heurísticas. Acá la lista [exporta] del manifiesto resuelve la ambigüedad al declarar. Tiene precedente directo: la hidratación del escritorio ya va por lista explícita (TARGETS="…"), nunca por glob.

Capa 3 — el grafo lo sabe. Los nodos exportados entran en build-state.json con clase ajeno, y el reporte pasa a decir:

escritorio-kde   171 nativas + 6 ajenas    (ajenas: sin procedencia de fuente, sin repro)

Bedrock no puede decirte qué tenés. Nosotros sí, y esto cierra el riesgo real de todo el ADR: que en seis meses alguien cuente esas apps como parte del corpus. En otra clase no se pueden contar mal.

D6 — El caso Steam, que es el que ordena el diseño

Steam ya es un contenedor: el cliente corre en el sistema y los juegos corren dentro de pressure-vessel, el contenedor bwrap de Valve con el runtime sniper. La forma correcta no es meter Steam en nuestra jaula sino anidarlas:

arje-zero → kwin (wayland)
  └─ instancia glibc (tipo 1: drivers, libs de 32 bits, dnf)
       └─ cliente Steam
            └─ pressure-vessel (tipo 2, de Valve)      ← anidado
                 └─ el juego + Proton

Corre mejor así porque es la configuración exacta que Valve prueba. Nosotros ponemos el nivel de afuera y la política; adentro no tocamos nada.

⚠ Verificar, no asumir: pressure-vessel necesita crear user namespaces desde dentro de uno ya creado. El kernel los tiene encendidos, pero el anidamiento hay que probarlo con un juego real. Es la clase de cosa que sella verde y falla en la mano del usuario.

D7 — La política se escribe, y es la única vez

harkaq deriva la política en vez de escribirla: política = clausura(deps declaradas), y de ahí sale la evidencia negativa que hace valioso al subproducto (SDD 16 §3).

Una imagen ajena no tiene clausura declarada. Es el primer y único lugar del sistema donde la política hay que autorarla. No es un defecto: es una excepción que conviene nombrar y acotar en vez de descubrir. El bloque [concesiones] del manifiesto es esa política, por defecto vacío, y se compila al mismo PolicySpec que ya consume harkaq-exec.

Un dnf que no alcanza ~/.ssh es un argumento que ninguna distro ofrece.

D8 — Qué se pinea (el suelo, no la instancia) y cuántas imágenes se curan

Dos preguntas que se confunden y tienen respuestas distintas.

a) Pinear no congela: fija el punto de partida. El digest ES la identidad, igual que alpine-minirootfs, zig y el rootfs del lab. Lo que queda dentro y fuera del pin:

qué¿pineado?cómo se actualiza
el rootfs basesí, por sha256cambiar el digest en instancia.toml + hammer qorpa recreate
lo que instalás adentro (dnf install steam, pacman -Syu)no, y no puede estarlocon el gestor de la imagen, cuando quieras

Subir la base de versión es barato precisamente por D3: como el manifiesto es la verdad y el upper/ es caché, se cambia base = "sha256:…" y se recrea. Sin D3 sería un rebase sobre un blob irreemplazable; con D3 son dos líneas y un rato de red.

El riesgo real del pin —que upstream borre el tarball— ya está resuelto por ADR 0013: la URL no entra en la identidad, sólo el sha256, así que espejar una imagen es gratis y una imagen pineada no se puede perder.

b) Pinear ≠ lista cerrada. hammer qorpa pull <url> --sha256 acepta cualquier rootfs. Lo corto no es lo que se puede traer, sino lo que nosotros probamos, espejamos y publicamos, porque cada imagen curada es una segunda cadena de suministro que hay que sostener (§NO-resuelve 3). Se curan tres, y cada una entra por un trabajo distinto — no por sabor:

imagentipoqué trabajo hacepin (verificado 2026-09-03)
Arch bootstrap1juegos: multilib de 32 bits de primera clase, y SteamOS es Arch ⇒ extiende el argumento de D6 («la configuración exacta que Valve prueba»)archlinux-bootstrap-2026.09.01-x86_64.tar.zst, sha256 895661bd…, en archive.archlinux.org (archivado para siempre)
Ubuntu base LTS1binarios comerciales: Zoom, Slack, Discord, Spotify, Chrome, JetBrains se compilan y prueban contra estoubuntu-base-24.04.3-base-amd64.tar.gz, sha256 6bc2cde3…, cdimage.ubuntu.com + old-releases
Steam Runtime «sniper»2la única sellable: al no mutar entra al store por file_drop y su digest va al índice firmado — cadena de custodia nuestra⚠ pin sin verificar todavía

Fedora queda BYO, no curada. No por calidad —publica Fedora-Container-Base-Generic-43-1.6 con CHECKSUM firmado y se pinea igual de bien— sino porque hace el mismo trabajo que Arch en el slot «fresco», y una tercera cadena mutable es justo la normalización que §NO-resuelve 3 quiere evitar. El ejemplo de D3 sigue diciendo fedora-43 a propósito: el mecanismo no distingue, y eso es la demostración de que la terna es una decisión de curaduría, no del diseño.

c) Compartibles, más adelante. Una imagen pineada es cuerpo inmutable direccionado por contenido, así que ADR 0014 se le aplica sin cambiar nada: puede servirse desde cualquier origen porque el digest la verifica, y ausencia ⇒ seguir mientras que contenido distinto ⇒ ABORTAR. No se hace el día uno, pero no hay que diseñar nada para que se pueda: lo único que falta es la decisión. ⚠ Antes de publicarlas a terceros (no a nuestras propias máquinas, que es lo que ya hace ADR 0013 con las fuentes) hay que mirar licencia y marca: redistribuir un rootfs de Ubuntu o Fedora sin modificar suele estar permitido, pero la política de marca es cosa aparte y no es una pregunta técnica.


Lo que este ADR admite que NO resuelve

Se escriben acá para que no se descubran en producción.

  1. El socket de Wayland es un borde de privilegio, no un caño. Pasarlo crudo le da a la imagen ajena todos los protocolos privilegiados que exponga el compositor: screencopy, virtual-keyboard, input-method. Flatpak no pasa el socket: pasa un proxy que filtra globals. No lo tenemos. Mientras no exista, una instancia con socket de Wayland puede capturar la pantalla y sintetizar teclas, y el manifiesto debe decirlo en la cara del usuario. Cerrarlo es tarea propia y probablemente el ticket más valioso que sale de este ADR.

  2. UID mapping. Sin userns privilegiados, root adentro es tu UID afuera — que es la propiedad de seguridad que queremos, pero rompe operaciones que hacen chown a UIDs distintos (rpm crea usuarios de servicio). La salida son rangos subuid, y las piezas ya están en el corpus: recipes/shadow.toml instala newuidmap/newgidmap y crea /etc/subuid vacío. Falta provisionarlo (capacidad de fichero o setuid + rango en /etc/subuid) y probarlo con un dnf install real. Es la primera cosa que va a fallar.

  3. Segunda cadena de suministro, sin ninguna garantía de hammer. Sin repro, sin cierre firmado, sin escaneo de licencias. El riesgo no es técnico: es que se normalice. Mitigación = D5 capa 3 (clase ajeno en el grafo) + un renglón explícito en SDD 20: las imágenes ajenas no entran en el catálogo publicable ni en el reporte de licencias, porque no podemos enumerarlas.

  4. El alcance del claim. Desde el día que exista una instancia, la frase «hammer reproduce bit a bit» hay que acotarla por escrito: el sistema base reproduce; las instancias ajenas no, y se declaran como tales. Sin eso, la cultura de números honestos se erosiona sola — que es exactamente la falla que ya nos pasó con el store-gc y con la deuda fantasma.

  5. Disco. Un rootfs de Fedora son 1-2 G y no lo alcanzan store-gc ni .dmerge. Necesita poda propia. Este repo ya tuvo tres emergencias de disco; que la poda nazca con el subsistema, no después.

  6. Red adentro. dnf la necesita. Es la concesión más peligrosa del manifiesto y por eso es la única que se escribe sola en una línea aparte. También hace falta /etc/resolv.conf: es el segundo fallo clásico de la primera corrida.

  7. X entre instancias. Un Xwayland dentro de una instancia levanta su servidor X. Portapapeles con el escritorio funciona (pasa por el compositor); entre dos instancias, no. Aceptado.


Lo que se cae, y lo que se resuelve gratis

Lo que este ADR NO cambia


Orden de trabajo propuesto

  1. Provisionar subuid y probar dnf install en una instancia mínima. Es lo primero que falla (§NO-resuelve 2) y define si el resto es fácil o difícil.
  2. hammer qorpa pull <url> --sha256 + verificación de digest. Reusa el patrón de lab-image.sh.
  3. Instancia = overlay sobre la imagen + instancia.toml; create / recreate / run.
  4. Concesiones → PolicySpec de harkaq. Empezar por nada y abrir sólo lo declarado.
  5. Shims + .desktop generados (export), y clase ajeno en build-state.py.
  6. Steam de punta a punta, con verificación explícita del bwrap anidado.
  7. Poda (hammer qorpa prune) y renglón en SDD 20 sobre licencias.
  8. Proxy filtrante de Wayland — ticket propio, el más valioso de la lista.

Nombre — ADOPTADO 2026-09-03

El subsistema se llama qorpa — huésped alojado: alguien que entra a la casa, se le dan habitaciones concretas y no las demás. Elegido por el autor del repo; entra en la familia (harkaq, yupana, arje, wawafs, kikin, churay, mirada, minga).

Consecuencias de nombre, ya aplicadas arriba: la frontera es hammer qorpa {…}, el espacio de nombres es /var/lib/hammer/qorpa/{imagenes,instancias}/ —uno solo, para que la poda de §NO-resuelve 5 tenga un único árbol que barrer— y el adjetivo del grafo sigue siendo ajeno (clase de nodo), porque describe la procedencia del nodo, no el subsistema que lo aloja.