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

Paso 2 del §Orden de trabajo del ADR 0015. Verbos en inglés (regla 4); el ADR
decía traer/crear/correr y queda corregido, con una línea que dice por qué para
que no se vuelva a proponer.

`hammer qorpa pull <url> --sha256 <sha>` baja, VERIFICA y recién entonces
desempaca — nunca al revés: un tar ajeno sin verificar es código ajeno que ya
escribió en tu disco. Veredicto de ADR 0014: contenido distinto ⇒ ABORTAR, y no
queda nada a medias. La identidad es el sha256 del ARCHIVO, no del árbol, así
que la URL es informativa y espejar sale gratis (ADR 0013). `list` marca a
gritos las imágenes vacías y sale ≠0 (regla 3). Los pasos 3-7 están declarados
en la superficie y fallan diciendo a qué paso del ADR pertenecen.

Nada de esto toca el store: es el espacio paralelo /var/lib/hammer/qorpa (D1).

PROBADO de punta a punta contra las dos imágenes curadas — Ubuntu base 24.04.3
(2760 ficheros, 78 M) y Arch bootstrap 2026.09.01 (31748, 534 M), las dos con
su glibc adentro, que es el montón B entero. Y probarlo de verdad destapó tres
cosas que en verde no se ven:

1. `-p` sin `--delay-directory-restore` NO extrae un rootfs real sin ser root:
   /etc/ca-certificates/extracted/cadir es 0555 y tar lo crea con su modo final
   ANTES de llenarlo.
2. Mi limpieza mentía: `remove_dir_all().ok()` no puede con un árbol que trae
   directorios de sólo-lectura, así que el staging de un pull roto SOBREVIVÍA y
   el siguiente pull extraía encima. El síntoma («Permission denied» en un
   directorio recién creado) no se parece en nada a la causa.
3. Renombrar un DIRECTORIO exige escritura sobre el directorio mismo, y el
   root.x86_64 de Arch viene dr-xr-xr-x. Se abre, se mueve y se le devuelve su
   modo exacto.

Y una regla que sonaba razonable y era falsa: «si hay un solo directorio arriba,
ése es el rootfs». El bootstrap de Arch trae TRES entradas arriba (root.x86_64,
version, pkglist) ⇒ no disparaba y el rootfs quedaba un nivel abajo, con todo
verde y sin un error. Ahora se ancla por ESTRUCTURA (tiene etc/ y usr|bin), con
--subdir como escape, y si no acierta FALLA en vez de adivinar: un rootfs mal
anclado no rompe acá, rompe cuando la instancia no encuentra su loader. Los
hermanos descartados quedan escritos en el manifiesto, no tirados en silencio.

5 tests nuevos, incluida la cicatriz de Arch. 35/35 en hammer-cli.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
This commit is contained in:
Sergio
2026-09-03 04:02:46 +00:00
co-authored by Claude Opus 5
parent 9a2739f856
commit fe155cf20d
4 changed files with 656 additions and 8 deletions
+10 -8
View File
@@ -3,9 +3,11 @@
- **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 {traer,crear,correr,exportar,listar,podar}`,
- **Frontera (a crear):** `hammer qorpa {pull,create,run,export,list,prune}`,
`/var/lib/hammer/qorpa/imagenes/<sha256>/`, `/var/lib/hammer/qorpa/instancias/<id>/`,
`instancia.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](../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).
@@ -145,7 +147,7 @@ apps = ["steam.desktop"]
```
Consecuencias que se caen solas:
- **`hammer qorpa recrear <id>`** reconstruye la instancia desde el manifiesto. El `upper` es
- **`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
@@ -244,7 +246,7 @@ Dos preguntas que se confunden y tienen respuestas distintas.
| qué | ¿pineado? | cómo se actualiza |
|---|---|---|
| el rootfs base | **sí**, por sha256 | cambiar el digest en `instancia.toml` + `hammer qorpa recrear` |
| el rootfs base | **sí**, por sha256 | cambiar el digest en `instancia.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
@@ -255,7 +257,7 @@ El riesgo real del pin —que upstream borre el tarball— **ya está resuelto p
[ADR 0013](0013-mirror-de-fuentes.md)**: 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 traer <url> --sha256` acepta cualquier rootfs. Lo corto
**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:
@@ -353,12 +355,12 @@ Se escriben acá para que no se descubran en producción.
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 traer <url> --sha256` + verificación de digest. Reusa el patrón de `lab-image.sh`.
3. Instancia = overlay sobre la imagen + `instancia.toml`; `crear` / `recrear` / `correr`.
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 (`exportar`), y clase `ajeno` en `build-state.py`.
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 podar`) y renglón en SDD 20 sobre licencias.
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