Files
takana/docs/adr/0015-imagenes-ajenas.md
T
SergioandClaude Opus 5 89da9c822f qorpa: el mapeo por rango — subuid + --userns FD, y el impuesto se paga
Resuelve §NO-resuelve 2 del ADR 0015, que era el ticket que más desbloqueaba.
Tres síntomas que parecían distintos —apt sin poder bajar a `_apt`, pacman sin
poder chownear a `alpm`, pressure-vessel sin poder escribir su uid_map— eran la
misma causa: bwrap crea el userns con UN SOLO id.

La cura resultó tener TRES partes, y ninguna sobra:

1. `setcap cap_setuid+ep newuidmap` (+ cap_setgid en newgidmap). shadow.toml los
   instala pero no los provisiona; sin la capability no escriben el mapa.
2. Crear el userns nosotros, mapear el rango de /etc/subuid con newuidmap y
   pasárselo a bwrap con `--userns FD`. bwrap crea el suyo con un solo id A
   PROPÓSITO y nunca llama a newuidmap: el trabajo es de quien lo invoca. El fd
   lo abre la shell (`exec 3<…`), porque un fd sólo cruza el exec si no es
   CLOEXEC y no valía la pena una dep de C para un fcntl.
3. Devolver las capabilities DENTRO del namespace. Ésta no estaba en el plan y
   es la que costó: bwrap las tira todas, y en Linux ser root es tener
   CAP_SETUID, no tener uid 0. Sin ella apt seguía sin poder seteuid(42) — un
   síntoma que parecía de subuid y no lo era. Son seguras por construcción:
   dentro de un userns sólo alcanzan lo que ese namespace posee, o sea nuestros
   propios subuid. CAP_SYS_ADMIN queda fuera y sigue colgando de `nesting`.

MEDIDO después: uid_map de 65537 ids, setgroups: allow, apt instala SIN el
APT::Sandbox::User=root, pacman sincroniza con DownloadUser=alpm INTACTO, y el
userns anidado monta con root=true ⇒ el conflicto 2 de D9 se disuelve solo.

Dos cosas más que salieron por medir, no por pensar:

- El guardián MENTÍA. qorpa-preflight envolvía al hijo en `timeout`, que forkea,
  así que newuidmap apuntaba al PID equivocado y el kernel respondía "Operation
  not permitted" — un falso negativo idéntico a un fallo real. Decía que subuid
  no andaba cuando a mano andaba. Ahora sale exit 0.
- Quitar el impuesto MUEVE el problema: el upper pasa a contener ficheros de los
  subuid (apt deja los suyos con uid 165577) que nuestro uid no puede borrar ⇒
  recreate entra a un userns mapeado para limpiar. Y cuando no hay rango, se
  degrada diciendo la causa exacta en vez de quedar en misterio.

Y un detalle que no es cosmético: `--perms 1777` antes del `--tmpfs /tmp`, o el
_apt al que apt baja no puede escribir su fichero temporal. Un /tmp que no es
1777 no es /tmp.

2 tests nuevos (el rango se lee por usuario; CAP_SYS_ADMIN NO está en las caps
de root, o `nesting` dejaría de ser una decisión). 45/45.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 05:27:03 +00:00

26 KiB

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

  • Estado: PROPUESTO — nombre ADOPTADO (qorpa, 2026-09-03); implementación EN CURSO por el §Orden de trabajo. Este documento decide la frontera; el código viene después.
  • Fecha: 2026-09-03
  • Frontera (a crear): hammer qorpa {pull,create,run,export,list,prune}, /var/lib/hammer/qorpa/images/<sha256>/, /var/lib/hammer/qorpa/instances/<id>/, instance.toml (manifiesto), clase de nodo ajeno en build-state.py.
  • Superficie: verbos en inglés, mensajes en castellano — CLAUDE.md regla 4. Este ADR nació proponiendo traer/crear/correr y se corrigió el 2026-09-03; el nombre qorpa sí es quechua.
  • Continúa: SDD 04 (overlay), SDD 16 (harkaq), ADR 0004 (hammer no usa nix), SDD 20 (catálogo publicable).
  • Absorbe: 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 + crates/hammer-overlay overlayfs, hammer try
política de grano fino en el kernel SDD 16 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/qorpa/images/<sha256>/    imagen  — INMUTABLE, verificada por digest
/var/lib/hammer/qorpa/instances/<id>/
    ├── instance.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:

Los campos van en inglés como el resto de la superficie (CLAUDE.md regla 4):

# /var/lib/hammer/qorpa/instances/games/instance.toml
base     = "sha256:…"                 # digest de la imagen, inmutable
distro   = "arch"                     # informativo
packages = ["steam", "mesa"]

[grants]                              # POR DEFECTO NADA
sockets = ["wayland", "pipewire"]
devices = ["/dev/dri", "/dev/ntsync"]
dirs    = [{ host = "~/Juegos", inside = "/home/user/Juegos", mode = "rw" }]
network = true                        # lo necesita el gestor de paquetes y lo necesita Steam

[export]                              # capa de transparencia; ver D5
binaries = ["steam"]
apps     = ["steam.desktop"]

Consecuencias que se caen solas:

  • hammer qorpa recreate <id> 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/qorpa/instances/*/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) no «literalmente lo que sea»
2 runtime curado congelado (Steam Linux Runtime sniper, runtime freedesktop) no , file_drop por sha256 cadena de custodia nuestra
3 bundle por app (AppImage) no 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;
  • inspeccionablecat 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 [export] 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 [grants] 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 base , por sha256 cambiar el digest en instance.toml + hammer qorpa recreate
lo que instalás adentro (dnf install steam, pacman -Syu) no, y no puede estarlo con 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:

imagen tipo qué trabajo hace pin (verificado 2026-09-03)
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)
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
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

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.

D9 — Qué aporta harkaq a una instancia, y los dos conflictos que sólo se ven midiendo

Honestidad primero: en el eje del sistema de ficheros, harkaq casi no agrega nada. El namespace de montaje de bwrap ya es una lista blanca —lo que no se bindea no existe adentro, y un --ro-bind es de sólo lectura por el kernel— así que escribir reglas Landlock que repiten eso sería un sello de goma. Por eso la política de una instancia no sellada es una sola línea (rw /) y no finge.

Lo que harkaq sí aporta, y pesa mucho más con un binario ajeno y opaco que con nuestros propios builds:

aporte por qué importa acá
seccomp bwrap no instala filtro alguno: hoy una instancia puede io_uring, bpf, ptrace, process_vm_readv, userfaultfd, keyctl, perf_event_open, init_module, kexec. Ésa es superficie de kernel contra un prebuilt de terceros
no_new_privs ningún setuid de la imagen ajena escala adentro
canal de evidencia con Landlock ABI ≥ 7 el kernel audita las denegaciones ⇒ se puede saber qué intentó tocar un binario cerrado. Es la única forma de auditar el montón B (leer ese canal es trabajo aparte: pide el lector con CAP_AUDIT_READ)
seal_image lo único del eje fs que el namespace no da: congelar /usr, /bin, /lib, /opt aunque adentro seas root, para que un exploit no persista en el upper

Conflicto 1 — Landlock y los contenedores anidados son incompatibles hoy. Medido: con un dominio Landlock activo, mount falla con EACCES aunque seccomp lo permita; el kernel no admite montajes nuevos bajo un dominio, porque escaparían de sus reglas por-ruta. Es decir: pressure-vessel no arranca bajo Landlock (D6). Por eso nesting = true cuesta la política de ficheros —harkaq-exec --allow-nesting --no-landlock—, y seccomp y no_new_privs siguen puestos, que es lo que más pesa. Nunca por defecto y siempre dicho en la línea de arranque: harkaq no afirma una jaula que no puso.

Conflicto 2 — root adentro y anidar se pelean, RESUELTO el mismo día. Con el userns de un solo id que crea bwrap, --uid 0 impedía que un userns anidado escribiera su uid_map: la instancia de juegos y la de gestor de paquetes querían mapeos opuestos. Con el mapeo por rango de §NO-resuelve 2 el conflicto desaparece —anida con root = true, verificado— y root deja de ser una elección incómoda. El campo se queda en el manifiesto porque sigue describiendo quién sos adentro, no porque haya que elegir entre dos males.

Y un detalle que ordena las expectativas: bwrap tira todas las capabilities, así que dentro de una instancia no hay CAP_SYS_ADMIN — el anidamiento sólo puede ser por userns sin privilegios, que es exactamente como lo hace pressure-vessel.


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 — el muro RESUELTO 2026-09-03. Este punto ya no describe un hueco; queda por lo que enseñó. La instancia veía uid_map: 0 1001 1 y setgroups: deny, y eso rompía apt (no podía bajar a _apt), pacman (no podía chownear su descarga a alpm) y el userns anidado de pressure-vessel. Tres síntomas, una causa.

    La cura entera —hammer qorpa run la aplica sola cuando puede— tiene tres partes, y ninguna sobra:

    pieza por qué
    setcap cap_setuid+ep newuidmap (y cap_setgid en newgidmap) el binario está en el corpus (recipes/shadow.toml) pero sin provisionar; sin la capability no puede escribir el mapa
    crear el userns nosotros, mapear el rango de /etc/subuid y pasárselo a bwrap con --userns FD bwrap crea el suyo con un solo id a propósito y nunca llama a newuidmap: el trabajo es de quien lo invoca
    devolver las capabilities dentro del namespace (--cap-add) bwrap las tira todas, y en Linux ser root es tener CAP_SETUID, no tener uid 0. Sin esto apt sigue sin poder seteuid(42) — un síntoma que parecía de subuid y no lo era

    Con las tres: uid_map de 65537 ids, setgroups: allow, apt instala sin el APT::Sandbox::User=root, pacman sincroniza con DownloadUser = alpm intacto, y el userns anidado monta con root = true ⇒ el conflicto 2 de D9 se disuelve. Las capabilities concedidas son seguras por construcción: dentro de un user namespace sólo alcanzan a lo que ese namespace posee, o sea nuestros propios subuid. CAP_SYS_ADMIN queda fuera y cuelga de nesting.

    Y una consecuencia que sólo aparece después: el upper/ pasa a contener ficheros de los subuid (apt deja los suyos con uid 165577) que nuestro uid normal no puede borrarrecreate y la poda tienen que entrar a un userns mapeado para limpiar (unshare -U --map-auto -r rm -rf). Quitar el impuesto mueve el problema, no lo evapora.

  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

  • 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. HECHO 2026-09-03. hammer qorpa pull <url> --sha256 + verificación de digest antes de desempacar. El rootfs se ancla por estructura (etc/ + usr|bin), con --subdir de escape.
  3. HECHO 2026-09-03. Instancia = overlay sobre la imagen + instance.toml; create / recreate / run. El overlay lo monta bwrap (--overlay-src + --overlay) dentro de su propio namespace: sin root y sin montar nada en el host. run entra con --clearenv y con --unshare-net salvo que se declare network — el entorno del host también es una concesión.
  4. HECHO 2026-09-03. Concesiones → política de harkaq. harkaq-exec entra como último eslabón dentro de bwrap (cruza un binario estático, no una librería ⇒ D2 sigue en pie), y aporta lo que bwrap no da: seccomp —hoy una instancia podría io_uring, bpf, ptrace, userfaultfd, keyctl, perf_event_open—, no_new_privs, y el canal de evidencia. Ver D9.
  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 qorpahué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.