diff --git a/docs/adr/0015-imagenes-ajenas.md b/docs/adr/0015-imagenes-ajenas.md new file mode 100644 index 00000000..b42e25a0 --- /dev/null +++ b/docs/adr/0015-imagenes-ajenas.md @@ -0,0 +1,322 @@ +# ADR 0015 — Imágenes ajenas: el mundo glibc entra enjaulado, y no entra al store + +- **Estado:** PROPUESTO — sin implementar. Este documento decide la frontera; el código viene después. +- **Fecha:** 2026-09-03 +- **Frontera (a crear):** `hammer ajenas {traer,crear,correr,exportar,listar,podar}`, + `/var/lib/hammer/ajenas//`, `/var/lib/hammer/instancias//`, + `instancia.toml` (manifiesto), clase de nodo `ajeno` en `build-state.py`. +- **Continúa:** [SDD 04](../04-overlay.md) (overlay), [SDD 16](../16-harkaq-jaula.md) (harkaq), + [ADR 0004](0004-no-custom-nix.md) (hammer no usa nix), [SDD 20](../20-catalogo-publicable-y-completa.md) + (catálogo publicable). +- **Absorbe:** [`plan-jaula-juegos.md`](../plan-jaula-juegos.md) §Capa 3. Su **F1** pasa a ser un + caso particular de este ADR (imagen de tipo 2); su **F0** (flatpak al catálogo) queda innecesaria; + su **F2** (glibc + multilib 32-bit desde fuente, «una campaña entera») queda **cancelada**. + +--- + +## 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ón | qué hay adentro | qué lo bloquea | +|---|---|---| +| **A** | Firefox, Chromium, LibreOffice, mpv, GIMP, Inkscape, Blender, OBS | **nada estructural**: son Wayland-nativos y compilables. Falta escribirlas. | +| **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. | +| **C** | herramental X11: `xdotool`, `xbindkeys`, `x11vnc`, Barrier | **Wayland**, 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: + +| pieza | dónde | estado | +|---|---|---| +| aislamiento de namespaces | `recipes/bwrap.toml` | sellada; ya es dep del lab de build | +| capa mutable sobre base inmutable | [SDD 04](../04-overlay.md) + `crates/hammer-overlay` | overlayfs, `hammer try` | +| política de grano fino en el kernel | [SDD 16](../16-harkaq-jaula.md) | Landlock + seccomp; fase 1 ✅, barrido con 0 irreducibles | +| rootfs ajeno pineado por sha256 | `scripts/lab-image.sh` | en producción desde 2026-08-10 | +| `newuidmap` / `newgidmap` / `/etc/subuid` | `recipes/shadow.toml` | **instalados**; falta provisionarlos | +| kernel con lo necesario | `recipes/linux-metal.toml:111-114` | `OVERLAY_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/ajenas// imagen — INMUTABLE, verificada por digest +/var/lib/hammer/instancias// + ├── 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 cruza | por qué | +|---|---|---| +| pantalla | socket `wayland-0` | protocolo, no ABI. La imagen trae su `libwayland`. | +| audio | socket de PipeWire | ídem | +| IPC | socket de D-Bus (opcional, por sesión) | ídem | +| GPU | `/dev/dri/*` | ABI de kernel (DRM), estable | +| juegos | `/dev/ntsync` | ABI de kernel | +| input | `/dev/input/*` (sólo si se concede) | ABI de kernel | +| **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. | + +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: + +```toml +# /var/lib/hammer/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: +- **`hammer ajenas recrear `** reconstruye la instancia desde el manifiesto. El `upper` es + descartable. +- **Actualizar la base** (Fedora 43 → 44) no es un rebase riesgoso: se cambia el digest y se recrea. +- **El respaldo** es el manifiesto (KB), no el `upper` (GB). `respaldo-storagebox.sh` no toca + `/var/lib/hammer/instancias/*/upper` y eso es correcto, no un olvido. +- La poda tiene una regla trivial: **un `upper` siempre se puede borrar.** + +### D4 — Granularidad: cuatro tipos, no uno + +| tipo | qué es | mutable | ¿sellable? | para qué | +|---|---|---|---|---| +| **1** | rootfs completo con gestor (Fedora+`dnf`, Arch+`pacman`) | sí | no | «literalmente lo que sea» | +| **2** | runtime curado congelado (Steam Linux Runtime *sniper*, runtime freedesktop) | **no** | **sí**, `file_drop` por sha256 | cadena de custodia nuestra | +| **3** | bundle por app (AppImage) | no | sí | una app suelta | +| **4** | glibc 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: +- **cero costo en runtime** — no hay proceso en el camino crítico de cada `exec`; +- **inspeccionable** — `cat` al shim y ves exactamente qué hace; +- **revocable** — se borran los shims y la instancia desaparece del sistema sin desmontar nada. + +⚠ **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](../16-harkaq-jaula.md) §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. + +--- + +## 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](../20-catalogo-publicable-y-completa.md): + **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 + +- **`xwayland` deja de ser deuda del corpus.** Si las apps X sólo corren dentro de instancias, el + Xwayland va **adentro de la imagen** —Fedora y Arch ya lo traen— y se cuelga de nuestro kwin por + el socket. El nodo `wanted` de `escritorio-kde` se disuelve sin escribir la receta y sin tocar la + postura Wayland-only. `targets.toml` debe sacarlo de las raíces con una nota que apunte acá. +- **GIMP e Inkscape dejan de reabrir GTK3.** Son GTK3 y GTK3 está aparcado a propósito + (ver la memoria del frente GNOME). Con instancias, corren sin traer el árbol GTK3 al corpus. +- **F2 del plan de juegos, cancelada.** Las libs de 32 bits las trae la imagen. +- **F0 del plan de juegos, innecesaria.** No hace falta meter flatpak+ostree al catálogo para tener + juegos. + +## Lo que este ADR NO cambia + +- **El montón A se sigue construyendo nativo.** La jaula es para binarios ajenos, no una excusa para + no empaquetar. Orden acordado: **mpv → OBS → Firefox**. +- **Wayland-only sigue en pie**, y este ADR es lo que lo hace barato. +- **arje-zero sigue siendo PID 1.** Nada de integración a nivel de init, que es una de las formas de + Bedrock que se rechazan explícitamente. +- **Cada instancia se gestiona con su propio gestor**, entrando explícitamente. Nada de multiplexar + `dnf`/`pacman` detrás de un comando único (el `pmm` de Bedrock): es menos mágico y no miente. + +--- + +## 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 ajenas traer --sha256` + verificación de digest. Reusa el patrón de `lab-image.sh`. +3. Instancia = overlay sobre la imagen + `instancia.toml`; `crear` / `recrear` / `correr`. +4. Concesiones → `PolicySpec` de harkaq. Empezar por **nada** y abrir sólo lo declarado. +5. Shims + `.desktop` generados (`exportar`), y clase `ajeno` en `build-state.py`. +6. **Steam de punta a punta**, con verificación explícita del bwrap anidado. +7. Poda (`hammer ajenas podar`) y renglón en SDD 20 sobre licencias. +8. Proxy filtrante de Wayland — ticket propio, el más valioso de la lista. + +## Nombre + +El subsistema pide un nombre de la familia del repo (harkaq, yupana, arje, wawafs, kikin, churay, +mirada, minga, khipu). Propuesta: **`qorpa`** — *huésped alojado*: alguien que entra a la casa, se +le dan habitaciones concretas y no las demás. Encaja con lo que hace y con cómo se comporta. Queda a +criterio del autor del repo; el resto del ADR no depende del nombre. diff --git a/docs/plan-jaula-juegos.md b/docs/plan-jaula-juegos.md index 0bb5bacb..85ff0310 100644 --- a/docs/plan-jaula-juegos.md +++ b/docs/plan-jaula-juegos.md @@ -55,6 +55,15 @@ verificación existente, como el 2b7c701. ## Capa 3 — la jaula glibc (el elefante, por fases) +> **⚠ GENERALIZADO por [ADR 0015 — Imágenes ajenas](adr/0015-imagenes-ajenas.md) (2026-09-03, +> PROPUESTO).** Esta capa razonaba sobre un runtime concreto de Valve; el ADR razona sobre +> cualquier rootfs ajeno y decide la frontera con el store. Si el ADR se acepta: **F1** queda como +> caso particular (imagen «de tipo 2»: curada, inmutable, sellable por `file_drop`), **F0** +> (flatpak+ostree al catálogo) queda innecesaria, y **F2** (glibc + multilib 32-bit desde fuente) +> queda **cancelada** — las libs de 32 bits las trae la imagen ajena. El resto de este plan +> (capas 1 y 2, todo lo nativo) no cambia. + + Steam, Proton y el catálogo de juegos son binarios glibc, parte 32-bit. musl no los va a correr y perseguir eso es un pozo. La respuesta con ADN hammer: **el runtime ajeno se trae entero, pineado por hash, y corre enjaulado** — verificar, no confiar, aplicado a un rootfs diff --git a/docs/state/targets.toml b/docs/state/targets.toml index f1d582f9..ed80d0c8 100644 --- a/docs/state/targets.toml +++ b/docs/state/targets.toml @@ -63,6 +63,11 @@ paquetes = [ # roto: sin tipos de fichero, sin apps X11, sin diálogos de autorización y con los controles QML # en un estilo genérico. Ver docs/state/frontera-triaje.toml para el porqué de cada uno. "shared-mime-info", "xwayland", "polkit-qt-1", "qqc2-breeze-style", + # ⚠ `xwayland` sigue acá pero su receta NO se escribe todavía: [ADR 0015 — Imágenes ajenas] + # (../adr/0015-imagenes-ajenas.md, PROPUESTO 2026-09-03) propone que el Xwayland viva DENTRO de + # la imagen ajena —Fedora y Arch ya lo traen y se cuelga de kwin por el socket— con lo cual este + # hueco se disuelve sin receta y sin tocar la postura Wayland-only. Si el ADR se acepta, sale de + # esta lista. Los otros tres ya están sellados (2026-09-02/03). ] [perfil.escritorio-gnome]