qorpa D8: el pin de sniper verificado, y el pin deja de ser una cita

Cierra el ⚠ que quedaba en la tabla de D8. Tres cosas, y la del medio es la que
más duele.

1. **Sniper pineado, y por la versión correcta.** No se pinea «la última» sino
   la que `latest-container-runtime-depot.txt` dice que despliega el cliente de
   Steam — 3.0.20260805.254768. Pinear otra sería pinear algo que nadie corre.
   Se pinean DOS artefactos: la imagen (rootfs, 302 MB, 11196 ficheros) y el
   depot `SteamLinuxRuntime_sniper.tar.xz` con pressure-vessel adentro. El
   segundo es el que destraba el paso 6: ese runtime sólo se baja al instalar un
   juego (credenciales), y pineado se coloca a mano ⇒ pressure-vessel se puede
   ejercitar sin cuenta, sin juego y sin pantalla.

2. **Los digests estaban en prosa y ABREVIADOS, y eso no es un pin.** Cuando la
   poda se llevó las imágenes, `895661bd…` no alcanzó para volver a traerlas:
   hubo que ir a buscar los sha256 otra vez a upstream. Ahora enteros y
   verificados en `docs/state/qorpa-imagenes.toml`, con de qué lista salieron y
   **si esa lista está firmada** — que no todas: Ubuntu firma su SHA256SUMS,
   Arch firma el tarball pero no la lista, y Valve no firma nada.

3. **`--retry` de curl no cubría el fallo que de verdad pasa.** Los 302 MB de
   sniper murieron al 73% con `HTTP/2 INTERNAL_ERROR` y curl NO reintentó: sin
   `--retry-all-errors` sólo considera transitorios los timeouts y los 5xx. Y
   aun reintentando, sin `-C -` cada intento vuelve a empezar de cero. Con las
   dos banderas el mismo pull sobrevivió dos cortes más y llegó. El parcial ya
   no se borra al fallar (es lo que permite reanudar); es seguro porque quien
   decide es el sha256 de después, y `prune` ya lo barre.

De paso, dos comprobaciones en vez de suposiciones: el rootfs de sniper viene en
`files/` con un hermano `metadata` y el anclaje por estructura lo elevó solo
(tercera forma real de empaquetado), y traer el depot como rootfs FALLA en vez
de adivinar («no encuentro un rootfs en el archivo»).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q
This commit is contained in:
Sergio
2026-09-03 19:56:29 +00:00
co-authored by Claude Opus 5
parent 6794604a76
commit 3f560de8af
3 changed files with 142 additions and 7 deletions
+24 -3
View File
@@ -309,19 +309,40 @@ fn pull(
/// Descarga a fichero. NO se reusa `download::fetch_url_to_file` a propósito: ése trae el cuerpo
/// entero a memoria (le sirve para un patch de KB) y acá hablamos de cientos de MB, con `--max-time`
/// de 300 s que un rootfs no respeta.
///
/// **`--retry` a secas no cubre el fallo que de verdad pasa.** Medido trayendo el runtime *sniper*
/// (302 MB): la CDN de Valve cortó el stream al 73% con `HTTP/2 INTERNAL_ERROR` (exit 92) y curl
/// **no reintentó**, porque sin `--retry-all-errors` sólo considera transitorios los timeouts y los
/// 5xx — un stream roto a media transferencia no entra en esa lista. Y aun reintentando, sin
/// `-C -` cada intento vuelve a empezar de cero: en un fichero de cientos de MB sobre una conexión
/// que se corta, eso no converge nunca.
///
/// Por eso el parcial **NO se borra al fallar**: es lo que hace que el siguiente intento reanude en
/// vez de rebajar todo. Es seguro porque quien decide no es el fichero sino el `sha256` que se
/// verifica después —un parcial corrupto no puede pasar por bueno— y porque el nombre lleva punto
/// delante, así que `list` no lo ve como imagen y `prune` ya lo barre como «resto de un pull a
/// medias».
fn download_to_file(url: &str, dst: &Path) -> Result<()> {
if let Some(p) = dst.parent() {
std::fs::create_dir_all(p)?;
}
let st = Command::new("curl")
.args(["-fL", "--retry", "3", "--retry-delay", "2", "--connect-timeout", "30",
"--progress-bar", "-o"])
.args(["-fL", "--retry", "5", "--retry-delay", "2", "--retry-all-errors",
"--connect-timeout", "30", "-C", "-", "--progress-bar", "-o"])
.arg(dst)
.arg(url)
.status()
.context("no pude ejecutar curl")?;
if !st.success() {
std::fs::remove_file(dst).ok();
let reanudable = std::fs::metadata(dst).map(|m| m.len()).unwrap_or(0);
if reanudable > 0 {
eprintln!(
" (quedaron {} bajados en {}: el siguiente intento reanuda desde ahí; \
`qorpa prune` los barre)",
human(reanudable),
dst.display()
);
}
bail!("curl falló bajando {url} (exit {:?})", st.code());
}
Ok(())
+20 -4
View File
@@ -275,11 +275,27 @@ no es lo que *se puede* traer, sino lo que **nosotros probamos, espejamos y publ
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) |
| imagen | tipo | qué trabajo hace | pin |
|---|---|---|---|
| **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 |
| **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`, 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`, `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 | **verificado 2026-09-03**: snapshot `3.0.20260805.254768` (el que despliega el cliente, no «el último»), imagen y depot |
**Los digests enteros están en [`docs/state/qorpa-imagenes.toml`](../state/qorpa-imagenes.toml), no
acá.** Estaban acá, abreviados —`895661bd…`— y cuando la poda se llevó las imágenes del disco no
alcanzaron para volver a traerlas: hubo que ir a buscarlos otra vez a upstream. **Un pin que no se
puede pegar en un comando no es un pin, es una cita.** El fichero además dice de qué lista de sumas
salió cada uno y **si esa lista viene firmada**, que es la única forma honesta de hablar de una
cadena de suministro ajena: Ubuntu firma su `SHA256SUMS`, Arch no firma la lista pero sí el tarball,
y **Valve no firma nada** — su garantía entera es TLS más el pin.
**Del sniper se pinean dos cosas, y la segunda es la que destraba el paso 6.** La imagen
(`Platform-…-runtime.tar.gz`, un rootfs) y el **depot** `SteamLinuxRuntime_sniper.tar.xz`, que trae
pressure-vessel más la imagen y es lo que Steam despliega en `steamapps/common/`. Ese runtime
**sólo se baja al instalar un juego**, y eso pedía credenciales: pineado se coloca a mano, y
pressure-vessel se puede ejercitar **sin cuenta, sin juego y sin pantalla**. El depot **no es un
rootfs**: se trae con `--verify-only`, y traerlo a secas falla el anclaje en vez de adivinar
(comprobado: «no encuentro un rootfs en el archivo»).
**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
+98
View File
@@ -0,0 +1,98 @@
# Las imágenes ajenas CURADAS (qorpa, ADR 0015 D8). Un pin por imagen, entero y verificado.
#
# ── POR QUÉ ESTE FICHERO EXISTE ─────────────────────────────────────────────────────────────────
# Los pines vivían en la tabla de D8, en prosa y ABREVIADOS (`895661bd…`). Cuando la poda se llevó
# las imágenes del disco, esos pines no alcanzaban para volver a traerlas: un sha256 truncado no es
# un pin, es una cita. Hubo que ir a buscar los digests otra vez a upstream — que esta vez estaban,
# pero es exactamente el trance del que el pin debía protegernos.
#
# Regla que sale de ahí: **un pin que no se puede pegar en un comando no es un pin.**
#
# ── QUÉ ES CADA CAMPO ───────────────────────────────────────────────────────────────────────────
# kind 1 = rootfs mutable (se le instala adentro con su gestor) · 2 = runtime inmutable
# sha256 LA IDENTIDAD. Es el sha del ARCHIVO, no del árbol (ver ADR 0015 §Orden 2), así que
# dos máquinas que lo traigan de orígenes distintos coinciden ⇒ ADR 0013 y 0014 se
# aplican sin cambiar nada: la URL no entra en la identidad y espejar es gratis.
# checksums de dónde salió el digest, para poder re-verificarlo contra upstream sin adivinar.
# signed si esa lista viene firmada. Se escribe porque §NO-resuelve 3 dice que ésta es una
# SEGUNDA cadena de suministro sin garantías nuestras: lo honesto es decir cuánta
# garantía trae cada una, y no traen la misma.
# verified fecha en que ALGUIEN lo bajó y el sha dio. No es la fecha de publicación.
#
# Los campos van en inglés como el resto de los manifiestos de qorpa (`image.toml`,
# `instance.toml`); los comentarios, en castellano — `CLAUDE.md` regla 4.
#
# ── LO QUE ESTE FICHERO NO ES ───────────────────────────────────────────────────────────────────
# No es una lista cerrada: `hammer qorpa pull <url> --sha256 <hex>` acepta cualquier rootfs (D8 b).
# Curada es la lista de lo que NOSOTROS probamos, espejamos y sostenemos, porque cada imagen es una
# cadena de suministro más que mantener. Fedora queda BYO a propósito: hace el mismo trabajo que
# Arch y una tercera cadena mutable es justo la normalización que el ADR quiere evitar.
[arch-bootstrap]
kind = 1
label = "archlinux-bootstrap-2026.09.01"
work = """
Juegos. Trae `multilib` de 32 bits de primera clase las libs que la F2 del plan de juegos daba por
«una campaña entera» y SteamOS *es* Arch, así que extiende el argumento de D6: adentro corre la
configuración que Valve prueba.
"""
url = "https://archive.archlinux.org/iso/2026.09.01/archlinux-bootstrap-2026.09.01-x86_64.tar.zst"
sha256 = "895661bdf6c64e91b7725874165fd05dd30c438d3ffec661671ab5cfb261ca58"
checksums = "https://archive.archlinux.org/iso/2026.09.01/sha256sums.txt"
signed = "el tarball tiene .sig PGP propio; la lista de sumas NO está firmada"
verified = "2026-09-03"
# El bootstrap trae TRES entradas arriba (`root.x86_64`, `version`, `pkglist.x86_64.txt`): el
# anclaje por estructura lo eleva solo. La regla vieja —«si hay un solo directorio arriba, ése es
# el rootfs»— no disparaba y dejaba el rootfs un nivel abajo con todo en verde.
[ubuntu-base]
kind = 1
label = "ubuntu-base-24.04.3"
work = """
Binarios comerciales. Zoom, Slack, Discord, Spotify, Chrome y JetBrains se compilan y se prueban
contra Ubuntu LTS: adentro corren sin que nadie tenga que adivinar contra qué glibc los ligaron.
"""
url = "https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.3-base-amd64.tar.gz"
sha256 = "6bc2cde3930ad088b3bb46fa45279e96d25bc3810f209850ecbe4722711874f9"
checksums = "https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/SHA256SUMS"
signed = "sí — SHA256SUMS.gpg junto a la lista"
verified = "2026-09-03"
# Cuando salga 24.04.5 el punto-release viejo se muda a old-releases.ubuntu.com. No importa: la URL
# no es la identidad (ADR 0013), el sha256 sí.
[steam-runtime-sniper]
kind = 2
label = "sniper-platform-3.0.20260805.254768"
work = """
El runtime en el que Valve corre los juegos. Es la única imagen INMUTABLE de la terna: no se le
instala nada adentro, así que es la única sellable entra al store por `file_drop` y su digest
puede ir al índice firmado, cadena de custodia nuestra y la única que no abre una segunda cadena
mutable.
"""
url = "https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20260805.254768/com.valvesoftware.SteamRuntime.Platform-amd64%2Ci386-sniper-runtime.tar.gz"
sha256 = "1a293c312215c59e4e5348ce210594cd7967d1a1fcaec6248a024e0878776f2b"
checksums = "https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20260805.254768/SHA256SUMS"
signed = "NO — ni .asc ni .gpg ni .sig; toda la garantía es TLS + el pin"
verified = "2026-09-03"
# El rootfs viene en `files/` con un hermano `metadata` al lado: tercera forma real de empaquetado,
# después de la plana (Ubuntu) y la de Arch. El anclaje por estructura la resolvió sin ayuda.
# La versión no se eligió por «la última»: `latest-container-runtime-depot.txt` dice cuál despliega
# de verdad el cliente de Steam, y ES ésta. Pinear otra sería pinear algo que nadie corre.
[steam-linux-runtime-sniper-depot]
kind = 2
label = "steamlinuxruntime-sniper-3.0.20260805.254768"
work = """
El bundle que el cliente de Steam despliega en `steamapps/common/SteamLinuxRuntime_sniper`: trae
pressure-vessel MÁS la imagen de arriba. Se pinea porque es lo que destraba el paso 6 del ADR el
runtime sólo se baja al instalar un juego, y eso pedía credenciales; pineado se coloca a mano y
pressure-vessel se puede ejercitar sin cuenta, sin juego y sin pantalla.
"""
url = "https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20260805.254768/SteamLinuxRuntime_sniper.tar.xz"
sha256 = "e264f0639ab775338311036f207b35cebe99bc417b016b53931ebca8b30b3d94"
checksums = "https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20260805.254768/SHA256SUMS"
signed = "NO — igual que arriba"
verified = "2026-09-03"
# NO es un rootfs: es un depot con `run`, `pressure-vessel/` y la imagen adentro. Traerlo con
# `pull` a secas tiene que FALLAR el anclaje en vez de adivinar; se trae con `--verify-only`, que
# guarda el archivo verificado sin desempacar.