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 1574Shell 204Rust 111Markdown 993763 Sutichaykuna

qorpa D3: `packages` se instala solo — el manifiesto deja de ser un adorno

Hasta acá el ADR afirmaba «el manifiesto es la verdad, el upper es caché»
mientras `recreate` confesaba en su propia salida que «instalarlos todavía es a
mano». Con eso el `upper` SÍ era el activo: un blob irreemplazable, que es justo
lo que hammer existe para no tener.

`hammer qorpa provision <id>` instala lo declarado, y **`recreate` lo llama
solo** (`--no-provision` para saltarlo).

Cuatro decisiones, cada una con su porqué:

- **El gestor se DETECTA** en la vista merged (apt, pacman, dnf, apk), no se
  configura: cada imagen trae el suyo. Y no se multiplexa detrás de un comando
  único —el `pmm` de Bedrock que el ADR rechaza—: se elige cuál correr.
- **`provision` ensancha la política y lo dice en la cara.** Instalar pide las
  tres cosas que una instancia bien declarada no tiene: red, root y la imagen
  sin sellar. Se ensancha SÓLO durante esa operación, el manifiesto no se toca
  y el siguiente `run` vuelve a lo escrito. En silencio sería lo que D7 prohíbe.
- **El registro vive FUERA del `upper`** (`provisioned.toml`): dentro se iría
  con la capa. Por eso `recreate` lo borra — un registro que afirma paquetes
  sobre una capa recién vaciada es la forma más pura del error de la regla 3.
- **Los nombres se validan y se comillan**: salen de un fichero que escribe una
  persona, así que `strace; rm -rf /` no llega al guión.

**Las dos manías que sólo salen provisionando de verdad:** el bootstrap de Arch
trae la mirrorlist ENTERA comentada (pacman muere con «no servers configured») y
el llavero sin inicializar (toda firma inválida). El guión pone el mirror geo
oficial avisando cuál, y hace `pacman-key --init && --populate` sólo si falta.
`apt` no necesita ni un workaround: es el dividendo del rango de subuid.

Probado de punta a punta en los dos gestores —apt sobre Ubuntu base, pacman
sobre el bootstrap de Arch—: instalan, el binario corre después con la red
apagada, y un `recreate` tira la capa y la deja igual. 27 tests verdes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q

50d1dbcc314c529ff4a6dd9b22c35b1e05476166 · ruwaq Sergio · (1 killa ñawpaq) · tayta 7e1474d13 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 15

crates/hammer-cli/src/qorpa.rs

2 qillqakuna · +312 −12

diff --git a/crates/hammer-cli/src/qorpa.rs b/crates/hammer-cli/src/qorpa.rs
index 3836d783..b4fd6d71 100644
--- a/crates/hammer-cli/src/qorpa.rs
+++ b/crates/hammer-cli/src/qorpa.rs
@@ -91,6 +91,9 @@ pub enum QorpaCmd {
     /// irreemplazable es justo lo que hammer existe para no tener.
     Recreate {
         id: String,
+        /// No reinstalar `packages` después de tirar la capa. Deja la instancia como recién creada.
+        #[arg(long)]
+        no_provision: bool,
         #[arg(long)]
         root: Option<PathBuf>,
     },
@@ -114,6 +117,23 @@ pub enum QorpaCmd {
         #[arg(last = true)]
         cmd: Vec<String>,
     },
+    /// [ADR 0015 D3] Instala en la instancia lo declarado en `packages`.
+    ///
+    /// **Es lo que vuelve cierta la frase «el manifiesto es la verdad».** Sin esto `packages` era
+    /// una lista que nadie leía: `recreate` tiraba la capa y lo instalado a mano no volvía, así que
+    /// el `upper` era el activo de verdad — exactamente el blob irreemplazable que hammer existe
+    /// para no tener.
+    Provision {
+        id: String,
+        /// Reinstala aunque el registro diga que esos mismos paquetes ya se instalaron.
+        #[arg(long)]
+        force: bool,
+        /// Imprime el guión que correría dentro de la instancia y sale.
+        #[arg(long)]
+        dry_run: bool,
+        #[arg(long)]
+        root: Option<PathBuf>,
+    },
     /// [ADR 0015 §Orden 5] Genera los shims y `.desktop` de lo declarado en `[export]`.
     ///
     /// **Se GENERAN, no se copian.** El `Exec=` de un `.desktop` ajeno es texto ajeno: copiarlo
@@ -157,9 +177,14 @@ pub fn run(cmd: QorpaCmd) -> Result<()> {
         QorpaCmd::Create { id, base, distro, root } => {
             create(&resolve_root(root), &id, &base, distro.as_deref())
         }
-        QorpaCmd::Recreate { id, root } => recreate(&resolve_root(root), &id),
+        QorpaCmd::Recreate { id, no_provision, root } => {
+            recreate(&resolve_root(root), &id, no_provision)
+        }
         QorpaCmd::Run { id, root, dry_run, no_jail, single_id, cmd } => {
-            run_instance(&resolve_root(root), &id, &cmd, dry_run, no_jail, single_id)
+            run_instance(&resolve_root(root), &id, &cmd, Ajustes { dry_run, no_jail, single_id, ..Default::default() })
+        }
+        QorpaCmd::Provision { id, force, dry_run, root } => {
+            provision(&resolve_root(root), &id, force, dry_run)
         }
         QorpaCmd::Export { id, into, remove, root } => {
             export(&resolve_root(root), &id, into.as_deref(), remove)
@@ -577,8 +602,9 @@ struct Instance {
     base: String,
     #[serde(skip_serializing_if = "Option::is_none")]
     distro: Option<String>,
-    /// Lo que debe estar instalado adentro. Declarativo: hoy se registra, instalarlo es el
-    /// siguiente paso. Sin esto, `recreate` no podría reconstruir nada.
+    /// Lo que debe estar instalado adentro. **Se instala solo**: `hammer qorpa provision` lo pone
+    /// con el gestor de la imagen, y `recreate` lo llama después de tirar la capa. Sin esto el
+    /// `upper` sería el activo y el manifiesto un adorno.
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     packages: Vec<String>,
     /// La política, autorada (D7). **Por defecto vacía**: una instancia nace sin ver nada.
@@ -853,7 +879,7 @@ fn write_instance(dir: &Path, inst: &Instance) -> Result<()> {
     Ok(())
 }
 
-fn recreate(root: &Path, id: &str) -> Result<()> {
+fn recreate(root: &Path, id: &str, no_provision: bool) -> Result<()> {
     let (dir, inst) = read_instance(root, id)?;
     let (sha, _) = resolve_image(root, &inst.base)?;
     for capa in ["upper", "work"] {
@@ -861,14 +887,162 @@ fn recreate(root: &Path, id: &str) -> Result<()> {
             .with_context(|| format!("no pude tirar {capa}/ de {id}"))?;
         std::fs::create_dir_all(dir.join(capa))?;
     }
+    // El registro vive FUERA del `upper` (si viviera dentro se iría con él y nunca sabríamos qué
+    // había), así que hay que borrarlo a mano: la capa se fue, así que no hay nada instalado.
+    // Dejarlo sería la forma más pura del error de la regla 3 — un registro que afirma presencia
+    // sobre un árbol vacío.
+    let _ = std::fs::remove_file(dir.join(PROVISION_LEDGER));
     println!("instancia {id} recreada sobre sha256:{} — capa mutable tirada", &sha[..12]);
-    if !inst.packages.is_empty() {
+    if inst.packages.is_empty() {
+        return Ok(());
+    }
+    if no_provision {
+        println!(
+            "  declara {} paquete(s) y NO se reinstalaron (--no-provision): `hammer qorpa \
+             provision {id}`",
+            inst.packages.len()
+        );
+        return Ok(());
+    }
+    // Acá es donde D3 deja de ser una aspiración: si `recreate` no reinstala, el `upper` es el
+    // activo y el manifiesto un adorno.
+    println!("  reinstalando lo declarado ({} paquete(s))…", inst.packages.len());
+    provision(root, id, false, false)
+}
+
+// ── provision: `packages` deja de ser una lista que nadie lee (D3) ──────────────────────────────
+
+const PROVISION_LEDGER: &str = "provisioned.toml";
+
+/// Lo que quedó instalado, para no reinstalar por gusto y para que `recreate` sepa qué rehacer.
+/// Vive junto al manifiesto y NO dentro del `upper`: si viviera dentro se iría con la capa.
+#[derive(Serialize, Deserialize, Default)]
+struct Provisionado {
+    manager: String,
+    packages: Vec<String>,
+    /// Segundos desde epoch. Sin dep de fechas: se lee con `date -u -d @<n>`.
+    at_unix: u64,
+}
+
+/// El guión que instala, y de qué gestor sale.
+///
+/// **Un gestor por imagen, entrando explícitamente** — el ADR rechaza multiplexar `dnf`/`pacman`
+/// detrás de un comando único (el `pmm` de Bedrock): eso es menos mágico y no miente. Acá no se
+/// multiplexa nada: se DETECTA cuál trae la imagen y se corre ése, con sus manías.
+#[derive(Debug)]
+struct Gestor {
+    nombre: &'static str,
+    guion: String,
+}
+
+/// Comillas simples de shell a prueba de todo: el contenido sale del manifiesto, que lo escribe una
+/// persona, y un nombre de paquete no tiene por qué ser un token seguro.
+fn comillar(s: &str) -> String {
+    format!("'{}'", s.replace('\'', r"'\''"))
+}
+
+fn gestor_de(dir: &Path, tree: &Path, paquetes: &[String]) -> Result<Gestor> {
+    // Nombres de paquete: se validan ANTES de armar el guión. Quotear alcanza para que no se
+    // ejecute nada, pero un nombre raro casi siempre es un error de tipeo, y fallar temprano dice
+    // dónde está mejor que un gestor quejándose de un paquete inexistente.
+    for p in paquetes {
+        if p.is_empty()
+            || !p.chars().all(|c| c.is_ascii_alphanumeric() || "._+-:@/".contains(c))
+        {
+            bail!("nombre de paquete raro en `packages`: {p:?} — se aborta antes de armar el guión");
+        }
+    }
+    let lista = paquetes.iter().map(|p| comillar(p)).collect::<Vec<_>>().join(" ");
+    let hay = |rel: &str| resolver_en_instancia(dir, tree, rel).is_some();
+
+    if hay("usr/bin/apt-get") {
+        return Ok(Gestor { nombre: "apt", guion: format!(
+            "set -e\n\
+             export DEBIAN_FRONTEND=noninteractive\n\
+             apt-get update\n\
+             apt-get install -y --no-install-recommends {lista}\n")});
+    }
+    if hay("usr/bin/pacman") {
+        // Dos manías del bootstrap de Arch, y ninguna se ve hasta que pacman falla:
+        //  · la mirrorlist viene ENTERA comentada ⇒ «no servers configured for repository»;
+        //  · el llavero no está inicializado ⇒ toda firma es inválida.
+        // El mirror se elige acá y se dice cuál: es el geo oficial de Arch. Que lo elija la
+        // herramienta es peor que que lo elija el usuario, pero **no elegir** deja una instancia
+        // que no instala nada, y el fichero queda a la vista en el `upper` para cambiarlo.
+        return Ok(Gestor { nombre: "pacman", guion: format!(
+            "set -e\n\
+             grep -q '^Server' /etc/pacman.d/mirrorlist 2>/dev/null || {{\n\
+             \x20 echo 'aviso: mirrorlist vacía — pongo el mirror geo oficial de Arch' >&2\n\
+             \x20 echo 'Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' >> /etc/pacman.d/mirrorlist\n\
+             }}\n\
+             [ -d /etc/pacman.d/gnupg ] || {{ pacman-key --init; pacman-key --populate archlinux; }}\n\
+             pacman -Sy --noconfirm --needed {lista}\n")});
+    }
+    if hay("usr/bin/dnf") {
+        return Ok(Gestor { nombre: "dnf", guion: format!(
+            "set -e\ndnf install -y --setopt=install_weak_deps=False {lista}\n")});
+    }
+    if hay("sbin/apk") || hay("usr/bin/apk") {
+        return Ok(Gestor { nombre: "apk", guion: format!(
+            "set -e\napk add --no-cache {lista}\n")});
+    }
+    bail!(
+        "no encuentro gestor de paquetes en la instancia (busqué apt-get, pacman, dnf y apk).\n  \
+         Si la imagen usa otro, instalá a mano con `run` — pero entonces `recreate` no lo va a \n  \
+         poder rehacer, y eso es justo lo que D3 dice que no puede pasar."
+    );
+}
+
+fn provision(root: &Path, id: &str, force: bool, dry_run: bool) -> Result<()> {
+    let (dir, inst) = read_instance(root, id)?;
+    let (_, tree) = resolve_image(root, &inst.base)?;
+    if inst.packages.is_empty() {
         println!(
-            "  declara {} paquete(s) ({}) — instalarlos todavía es a mano con `run`",
-            inst.packages.len(),
-            inst.packages.join(", ")
+            "la instancia {id} no declara `packages`. Se instala lo DECLARADO: es lo que hace que \
+             la capa mutable sea caché y no el activo."
         );
+        return Ok(());
     }
+
+    let previo: Option<Provisionado> = std::fs::read_to_string(dir.join(PROVISION_LEDGER))
+        .ok()
+        .and_then(|s| toml::from_str(&s).ok());
+    if let Some(p) = &previo {
+        if !force && p.packages == inst.packages {
+            println!(
+                "ya provisionada con {} ({} paquete(s)) — `--force` para reinstalar",
+                p.manager, p.packages.len()
+            );
+            return Ok(());
+        }
+    }
+
+    let g = gestor_de(&dir, &tree, &inst.packages)?;
+    if dry_run {
+        println!("# gestor detectado: {}", g.nombre);
+        print!("{}", g.guion);
+        return Ok(());
+    }
+    println!("provisionando {id} con {} — {} paquete(s)", g.nombre, inst.packages.len());
+    run_instance(root, id, &["/bin/sh".into(), "-c".into(), g.guion.clone()],
+                 Ajustes { provisionando: true, ..Default::default() })?;
+
+    let reg = Provisionado {
+        manager: g.nombre.into(),
+        packages: inst.packages.clone(),
+        at_unix: std::time::SystemTime::now()
+            .duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0),
+    };
+    std::fs::write(
+        dir.join(PROVISION_LEDGER),
+        format!(
+            "# Lo escribe `hammer qorpa provision`. Es un REGISTRO, no la verdad: la verdad es\n\
+             # `packages` en instance.toml. Si los dos difieren, gana el manifiesto y se reinstala.\n\
+             # `at_unix` se lee con `date -u -d @<n>`.\n{}",
+            toml::to_string_pretty(&reg).context("serializando el registro")?
+        ),
+    )?;
+    println!("listo: {} paquete(s) instalados con {}", reg.packages.len(), reg.manager);
     Ok(())
 }
 
@@ -1076,12 +1250,47 @@ fn userns_con_rango(uid_dentro: u32) -> Result<UsernsMapeado> {
     Ok(guard)
 }
 
+/// Lo que NO viene del manifiesto: banderas de la invocación. Van juntas para que agregar una no
+/// mueva la firma de media docena de llamadas.
+#[derive(Default, Clone, Copy)]
+struct Ajustes {
+    dry_run: bool,
+    no_jail: bool,
+    single_id: bool,
+    /// Modo `provision`: la política declarada se ENSANCHA para poder instalar, y sólo mientras
+    /// dura la instalación. Ver `provision()` — lo que se ensancha y por qué se dice en voz alta.
+    provisionando: bool,
+}
+
 fn run_instance(
-    root: &Path, id: &str, cmd: &[String], dry_run: bool, no_jail: bool, single_id: bool,
+    root: &Path, id: &str, cmd: &[String], aj: Ajustes,
 ) -> Result<()> {
-    let (dir, inst) = read_instance(root, id)?;
+    let Ajustes { dry_run, no_jail, single_id, provisionando } = aj;
+    let (dir, mut inst) = read_instance(root, id)?;
     let (sha, tree) = resolve_image(root, &inst.base)?;
 
+    // ── LA POLÍTICA SE ENSANCHA PARA PROVISIONAR, Y SE DICE ────────────────────────────────────
+    // Instalar paquetes pide exactamente las tres cosas que una instancia bien declarada NO tiene:
+    // red (los paquetes se bajan), root (el gestor chownea y escribe en /usr) y la imagen sin
+    // sellar (lo que instala va a /usr por definición). Ensancharlas en silencio sería la falla
+    // que el ADR llama por su nombre en D7: la política se AUTORA. Así que se ensancha sólo aquí,
+    // sólo mientras dura el `provision`, y se imprime en la cara — el manifiesto NO se toca, y el
+    // siguiente `run` vuelve a la política escrita.
+    if provisionando {
+        let mut abierto: Vec<&str> = Vec::new();
+        if !inst.grants.network { abierto.push("red"); inst.grants.network = true; }
+        if !inst.grants.root { abierto.push("root"); inst.grants.root = true; }
+        if inst.grants.seal_image { abierto.push("imagen sin sellar"); inst.grants.seal_image = false; }
+        if inst.run_as.is_some() { abierto.push("como root, no `run_as`"); inst.run_as = None; }
+        if !abierto.is_empty() {
+            eprintln!(
+                "⚠ provision ENSANCHA la política sólo para esta operación: {}. El manifiesto no \n\
+                 \x20 cambia y el próximo `run` vuelve a lo declarado.",
+                abierto.join(", ")
+            );
+        }
+    }
+
     // Quién sos adentro. Con el mapeo por rango esto NO cuesta el rango: lo decide el propio mapa.
     // ⚠ Cambiarlo en una instancia YA USADA la corrompe: los ficheros del `upper` quedaron con los
     // uids del mapa anterior. Para correr como usuario normal está `run_as`, que no toca el mapa.
@@ -2060,11 +2269,79 @@ mod tests {
         create(root, "i", &sha, None).unwrap();
         let upper = instance_dir(root, "i").join("upper");
         std::fs::write(upper.join("basura"), "x").unwrap();
-        recreate(root, "i").unwrap();
+        recreate(root, "i", true).unwrap();
         assert!(!upper.join("basura").exists(), "el upper no es caché si sobrevive");
         assert!(instance_dir(root, "i").join(INSTANCE_MANIFEST).exists(), "el manifiesto ES la verdad");
     }
 
+    /// El registro de lo provisionado vive FUERA del `upper`, así que `recreate` tiene que borrarlo
+    /// a mano. Si sobreviviera, afirmaría que hay paquetes instalados sobre una capa recién
+    /// vaciada: la forma más pura del error de la regla 3 —presencia declarada sin contenido—, y
+    /// además haría que el siguiente `provision` no instalara nada.
+    #[test]
+    fn recreate_no_deja_vivo_el_registro_de_lo_provisionado() {
+        let d = tempfile::tempdir().unwrap();
+        let root = d.path();
+        let sha = "e".repeat(64);
+        imagen_falsa(root, &sha);
+        create(root, "i", &sha, None).unwrap();
+        let dir = instance_dir(root, "i");
+        let (_, mut inst) = read_instance(root, "i").unwrap();
+        inst.packages = vec!["strace".into()];
+        write_instance(&dir, &inst).unwrap();
+        std::fs::write(
+            dir.join(PROVISION_LEDGER),
+            toml::to_string(&Provisionado {
+                manager: "apt".into(), packages: vec!["strace".into()], at_unix: 1,
+            }).unwrap(),
+        ).unwrap();
+
+        recreate(root, "i", true).unwrap();
+        assert!(!dir.join(PROVISION_LEDGER).exists(), "el registro sobrevivió a la capa que describía");
+    }
+
+    /// El gestor se DETECTA de la vista merged, no se configura: una imagen trae el suyo. Y el
+    /// `upper` gana, porque el gestor puede haberse instalado después.
+    #[test]
+    fn el_gestor_sale_de_la_imagen_y_el_upper_manda() {
+        let d = tempfile::tempdir().unwrap();
+        let dir = d.path().join("inst");
+        let tree = d.path().join("tree");
+        std::fs::create_dir_all(dir.join("upper/usr/bin")).unwrap();
+        std::fs::create_dir_all(tree.join("usr/bin")).unwrap();
+
+        std::fs::write(tree.join("usr/bin/apt-get"), "").unwrap();
+        let g = gestor_de(&dir, &tree, &["strace".into()]).unwrap();
+        assert_eq!(g.nombre, "apt");
+        assert!(g.guion.contains("--no-install-recommends"));
+
+        std::fs::write(tree.join("usr/bin/dnf"), "").unwrap();
+        assert_eq!(gestor_de(&dir, &tree, &["strace".into()]).unwrap().nombre, "apt",
+                   "con dos, manda el orden declarado, no el azar del directorio");
+
+        // Sin ninguno: falla diciendo qué buscó, en vez de correr un comando que no existe.
+        let vacio = d.path().join("pelada");
+        std::fs::create_dir_all(&vacio).unwrap();
+        let err = gestor_de(&vacio, &vacio, &["x".into()]).unwrap_err().to_string();
+        assert!(err.contains("pacman") && err.contains("apk"), "mensaje inesperado: {err}");
+    }
+
+    /// El nombre de paquete sale del manifiesto, que lo escribe una persona. Se comilla SIEMPRE, y
+    /// además se rechaza lo que no parece un nombre: quotear evita la ejecución, pero un `;` en un
+    /// nombre es un error de tipeo que conviene ver acá y no en la salida del gestor.
+    #[test]
+    fn un_nombre_de_paquete_raro_no_llega_al_guion() {
+        let d = tempfile::tempdir().unwrap();
+        let tree = d.path().join("tree");
+        std::fs::create_dir_all(tree.join("usr/bin")).unwrap();
+        std::fs::write(tree.join("usr/bin/apt-get"), "").unwrap();
+
+        for malo in ["strace; rm -rf /", "$(whoami)", "a b", "'", ""] {
+            assert!(gestor_de(d.path(), &tree, &[malo.into()]).is_err(), "pasó {malo:?}");
+        }
+        assert_eq!(comillar("a'b"), r"'a'\''b'");
+    }
+
     #[test]
     fn sin_concesiones_la_red_queda_fuera_y_con_red_entra_el_resolv_conf() {
         let (a, w) = grants_to_args(&Grants::default(), "/run/user/0", None);
diff --git a/docs/adr/0015-imagenes-ajenas.md b/docs/adr/0015-imagenes-ajenas.md
index aa19e3a6..f1afc387 100644
--- a/docs/adr/0015-imagenes-ajenas.md
+++ b/docs/adr/0015-imagenes-ajenas.md
@@ -156,6 +156,29 @@ Consecuencias que se caen solas:
   `/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.**
 
+**✅ COMPLETADO 2026-09-03 — hasta acá `packages` era una lista que nadie leía.** Durante unas
+horas el ADR afirmaba las cuatro consecuencias de arriba mientras `recreate` confesaba en su propia
+salida que «instalarlos todavía es a mano»: o sea que el `upper` **sí** era el activo y el
+manifiesto un adorno. Ahora `hammer qorpa provision <id>` instala lo declarado, y **`recreate` lo
+llama solo** (`--no-provision` para no hacerlo).
+
+| decisión | por qué |
+|---|---|
+| el gestor se **detecta** (`apt`, `pacman`, `dnf`, `apk`), no se configura | cada imagen trae el suyo. Y **no se multiplexa**: el ADR rechaza un comando único al estilo `pmm` de Bedrock; acá se elige cuál correr, no se inventa una interfaz que los tape |
+| `provision` **ensancha la política y lo dice en la cara** | instalar pide las tres cosas que una instancia bien declarada no tiene —red, root y la imagen sin sellar—. Se ensancha sólo durante esa operación, el manifiesto **no se toca**, y el siguiente `run` vuelve a lo escrito. Ensancharlo en silencio sería exactamente lo que D7 prohíbe |
+| el registro (`provisioned.toml`) vive **fuera** del `upper` | si viviera dentro se iría con la capa y nadie sabría qué había. Por eso `recreate` lo borra a mano: un registro que afirma paquetes sobre una capa recién vaciada es la forma más pura del error de la regla 3 |
+| los nombres de paquete se **validan y se comillan** | salen del manifiesto, que lo escribe una persona; `strace; rm -rf /` no llega al guión |
+
+**Las dos manías que sólo aparecen provisionando de verdad** (medidas, no leídas): el bootstrap de
+Arch trae la **mirrorlist entera comentada** —`pacman` muere con «no servers configured»— y el
+**llavero sin inicializar**, con lo que toda firma es inválida; el guión pone el mirror geo oficial
+**avisando cuál**, y corre `pacman-key --init && --populate` sólo si falta. `apt`, en cambio, no
+necesita ni un workaround: es el dividendo del rango de subuid.
+
+**Probado de punta a punta en los dos gestores**: `apt` sobre Ubuntu base y `pacman` sobre el
+bootstrap de Arch instalan, el binario corre después con la red apagada otra vez, y un `recreate`
+tira la capa y la deja igual — que es la prueba de esta sección entera.
+
 ### D4 — Granularidad: cuatro tipos, no uno
 
 | tipo | qué es | mutable | ¿sellable? | para qué |