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 815Rust 56Shell 37Markdown 291229 Definiciones

el saltito sin USB: kexec primero, espacio libre si la máquina no puede, y nunca desde adentro

Política pedida por el usuario para el caso «la partición destino ES la que estoy usando»:
intentar kexec; si esta máquina no puede saltar, instalar al lado en espacio libre; y si el destino
no es la raíz viva, flujo normal de siempre. Queda en --on-active-root (auto|kexec|free-space|abort).

1) 'takana kernel kexec --probe' — el salto ya estaba implementado (kexec_file_load por asm!, sin
   kexec-tools) pero no había forma de PREGUNTAR si se puede sin intentarlo. Quien decide no es quien
   salta: un instalador tiene que elegir camino antes de tocar nada. Exit 0 = sí.

   ⚠ Y el sondeo MIENTE si no sos root, medido sobre el MISMO kernel: como uid 1001 dijo «el syscall
   existe» y como root del anfitrión dijo ENOSYS. El kernel contesta EPERM antes de llegar a decir si
   implementa la llamada, así que un «sólo te falta ser root» puede estar tapando un «y además tu
   kernel no lo trae». El probe ahora lo dice en vez de sonar concluyente.

2) instalador-initramfs.sh — el instalador ENTERO en un initramfs que corre en RAM. Es lo que hace
   posible reemplazar el Linux que corre: se salta ahí por kexec, el sistema viejo muere en el salto
   y su disco queda libre. El payload viaja adentro porque el disco es justo lo que se va a
   formatear; por eso el script comprueba que entre en la RAM (>40 % ⇒ aborta y manda al USB), que
   es la diferencia entre un mensaje y un cuelgue mudo al desempaquetar.

3) --use-free-space — instalar AL LADO: busca el hueco sin asignar más grande y añade UNA entrada
   con 'sfdisk --append', sin tocar las existentes. Es la única vez que este script escribe la
   tabla, y se dice aparte.

4) TOOL_<binario>= en herramientas-desde-store.sh — dentro del initramfs no hay store del que
   resolver por hash ni host al que caer: los binarios sellados viajan copiados y se inyectan.

Tres hallazgos del banco, los tres con el mismo sabor —el síntoma señalaba al lugar equivocado—:

  a) 'partx -a' falla ENTERO («error adding partitions 1-4») cuando las viejas ya están registradas,
     que es siempre en este flujo. El que sirve es 'partx -u'. La opción que suena a «añadir» no es
     la que añade.
  b) 'sfdisk -d' ALINEA los campos con espacios ('start=     3614720'), así que buscar la partición
     nueva por 'start=<sector>' no casa nunca. El síntoma era idéntico a un device que no aparece y
     mandó a investigar udev y partx, que estaban bien. Ahora se detecta por diferencia de conjuntos.
  c) En el anfitrión musl no existen partprobe, partx ni blockdev ⇒ los tres intentos de releer la
     tabla eran no-ops silenciosos. Salen del util-linux sellado.

Verde en los dos escenarios del banco: instalar en partición existente y instalar al lado en el
hueco (tabla 3 → 4 entradas, las viejas sin moverse un sector, la vecina idéntica byte a byte).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

b9d86bc1a07855236e8c5094863f73bcce187b92 · por Sergio · (hace 29 d) · padre bea665954 · ver el árbol en este commit

Delta semántico · 6

crates/takana-cli/src/kernel_cmd.rs

5 archivos · +502 −23

diff --git a/crates/takana-cli/src/kernel_cmd.rs b/crates/takana-cli/src/kernel_cmd.rs
index cbee9d16..1c7d13df 100644
--- a/crates/takana-cli/src/kernel_cmd.rs
+++ b/crates/takana-cli/src/kernel_cmd.rs
@@ -234,8 +234,8 @@ pub enum KernelCmd {
     /// downtime y en un portátil no es nada. Se dice acá porque media industria vende esto como
     /// «live kernel update» y no lo es.
     Kexec {
-        /// Artefacto del kernel en el store (nombre de directorio o ruta).
-        artefacto: String,
+        /// Artefacto del kernel en el store (nombre de directorio o ruta). No hace falta con `--probe`.
+        artefacto: Option<String>,
         /// initramfs a cargar con él.
         #[arg(long)]
         initrd: Option<PathBuf>,
@@ -248,6 +248,13 @@ pub enum KernelCmd {
         /// Actually jump. Without this it only loads and reports.
         #[arg(long)]
         jump: bool,
+        /// Only answer whether this machine CAN kexec, and why not. Loads nothing, jumps nowhere.
+        ///
+        /// Existe porque quien decide no es quien salta: un instalador tiene que elegir el camino
+        /// **antes** de tocar nada, y la única forma honesta de saberlo era intentar el salto.
+        /// Exit 0 = se puede; exit 1 = no, con el motivo dicho.
+        #[arg(long)]
+        probe: bool,
     },
     /// Say where this boot came from and what is on trial.
     BootStatus {
@@ -288,8 +295,8 @@ pub fn run(cmd: KernelCmd, store: &str) -> Result<()> {
     match cmd {
         KernelCmd::Stage { artefacto, esp, efivars, tries, state, dry_run } =>
             ab_stage(&artefacto, store, &esp, &efivars, tries, &state, dry_run),
-        KernelCmd::Kexec { artefacto, initrd, cmdline, load_only, jump } =>
-            kexec_cmd(&artefacto, store, initrd.as_deref(), &cmdline, load_only, jump),
+        KernelCmd::Kexec { artefacto, initrd, cmdline, load_only, jump, probe } =>
+            kexec_cmd(artefacto.as_deref(), store, initrd.as_deref(), &cmdline, load_only, jump, probe),
         KernelCmd::BootStatus { efivars, state } => ab_status(&efivars, &state),
         KernelCmd::Confirm { esp, efivars, state } => ab_confirm(&esp, &efivars, &state),
         KernelCmd::Rollback { esp, efivars, state } => ab_rollback(&esp, &efivars, &state),
@@ -2145,14 +2152,93 @@ fn ab_rollback(esp: &Path, efivars: &Path, state: &Path) -> Result<()> {
     Ok(())
 }
 
+/// ¿Qué impide el salto en ESTA máquina? Las tres puertas, en el orden en que se cierran.
+///
+/// Se responde **preguntándole al kernel** (un `kexec_file_load` con fd inválido distingue ENOSYS de
+/// EBADF sin cargar nada) y leyendo `/sys/kernel/security/lockdown`, no adivinando por el `.config`:
+/// el `.config` que hay en `/boot` puede no ser el del kernel que corre.
+fn kexec_probe() -> (bool, Vec<String>) {
+    let mut motivos = Vec::new();
+    #[cfg(target_family = "unix")]
+    let root = unsafe { libc_getuid() } == 0;
+    #[cfg(not(target_family = "unix"))]
+    let root = true;
+
+    // ⚠ **La detección del syscall sólo es CONCLUYENTE como root, y da un falso positivo al revés.**
+    // Medido acá el 2026-09-23 sobre el MISMO kernel: como uid 1001 dentro de la jaula el sondeo dijo
+    // «el syscall existe» y como root del anfitrión dijo ENOSYS. La causa es el orden de los chequeos
+    // del kernel: sin CAP_SYS_BOOT contesta EPERM **antes** de llegar a decir que no lo implementa,
+    // y el sondeo sólo sabe distinguir ENOSYS de lo demás. O sea que un «te falta ser root» puede
+    // estar tapando un «además tu kernel no lo trae». Un NO sigue siendo fiable; un «todo bien menos
+    // root» no lo es, y decirlo es la diferencia entre un diagnóstico y una corazonada.
+    if !kernel_ab::kexec_file_disponible() {
+        motivos.push(
+            "el kernel que CORRE no implementa kexec_file_load (le falta CONFIG_KEXEC_FILE). \
+             Los kernels del corpus traen sólo el syscall viejo; la variante \
+             `recipes/linux-metal-kexec.toml` lo trae encendido."
+                .to_string(),
+        );
+    }
+    // El syscall existe pero lo cierra CAP_SYS_BOOT. Se dice aparte porque se arregla distinto
+    // (correr como root) y confundirlo con lo de arriba manda a recompilar un kernel que está bien.
+    if !root {
+        motivos.push(
+            "no somos root: kexec_file_load pide CAP_SYS_BOOT. Y OJO — sin privilegios el resto de \
+             este diagnóstico no es concluyente: el kernel contesta EPERM antes de decir si \
+             implementa el syscall, así que hay que repetir el probe COMO ROOT antes de creerle."
+                .to_string(),
+        );
+    }
+    // Lockdown es lo que enciende Secure Boot, y es la puerta que sorprende: el kernel tiene el
+    // flag, sos root, y aun así EPERM. En modo `integrity` o `confidentiality` el kernel sólo
+    // acepta una imagen FIRMADA, y las nuestras no lo están (ADR 0018, decisión abierta de firma).
+    if let Ok(t) = std::fs::read_to_string("/sys/kernel/security/lockdown") {
+        let t = t.trim().to_string();
+        if !t.contains("[none]") && !t.is_empty() {
+            motivos.push(format!(
+                "el kernel está en LOCKDOWN ({t}) — lo enciende Secure Boot, y en ese modo sólo \
+                 acepta kernels firmados"
+            ));
+        }
+    }
+    (motivos.is_empty(), motivos)
+}
+
+/// `getuid()` sin arrastrar la crate `libc` al workspace, igual que el syscall de kexec.
+#[cfg(target_arch = "x86_64")]
+unsafe fn libc_getuid() -> u32 {
+    let ret: i64;
+    unsafe {
+        std::arch::asm!("syscall", inlateout("rax") 102u64 => ret,
+            lateout("rcx") _, lateout("r11") _, options(nostack));
+    }
+    ret as u32
+}
+#[cfg(not(target_arch = "x86_64"))]
+unsafe fn libc_getuid() -> u32 { 0 }
+
 fn kexec_cmd(
-    artefacto: &str,
+    artefacto: Option<&str>,
     store: &str,
     initrd: Option<&Path>,
     cmdline: &str,
     load_only: bool,
     jump: bool,
+    probe: bool,
 ) -> Result<()> {
+    if probe {
+        let (puede, motivos) = kexec_probe();
+        if puede {
+            println!("kexec: SÍ — esta máquina puede saltar a otro kernel sin pasar por el firmware");
+            return Ok(());
+        }
+        println!("kexec: NO — no se puede saltar en esta máquina:");
+        for m in &motivos {
+            println!("  · {m}");
+        }
+        std::process::exit(1);
+    }
+    let artefacto = artefacto.ok_or_else(|| anyhow::anyhow!("falta el artefacto del kernel (o pasá --probe)"))?;
     let (bz, nombre) = bzimage_de(artefacto, store)?;
     println!("kexec: {nombre}");
     println!("  kernel:  {}", bz.display());
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
new file mode 100755
index 00000000..401fd489
--- /dev/null
+++ b/scripts/instalador-initramfs.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+# instalador-initramfs.sh — empaqueta el instalador ENTERO en un initramfs que corre en RAM.
+#
+# ── PARA QUÉ ───────────────────────────────────────────────────────────────────────────────────
+# Es lo que hace posible reemplazar el Linux que está corriendo **sin un USB**: se salta a este
+# initramfs por `kexec` (`takana kernel kexec --jump`, ADR 0017), el sistema viejo muere en el salto
+# y su disco queda libre ⇒ recién entonces su raíz se puede formatear. El mismo initramfs sirve
+# arrancado por QEMU o por un medio cualquiera; el salto es sólo una forma de llegar.
+#
+# ── POR QUÉ EL PAYLOAD VIAJA ADENTRO Y NO EN EL DISCO ──────────────────────────────────────────
+# Porque el disco es justamente lo que se va a formatear. Dejar el rootfs en la partición destino y
+# formatearla después es morderse la cola; dejarlo en otra partición ata la instalación a que exista
+# esa otra partición. En RAM no depende de nada: el precio es que el initramfs pesa lo que pese el
+# rootfs, y por eso el script COMPRUEBA que entre en la memoria de la máquina antes de armarlo.
+#
+# Uso:
+#   ROOTFS=<product-rootfs> scripts/instalador-initramfs.sh --root-part /dev/nvme0n1p2 --esp /dev/nvme0n1p1
+#   # o sin destino fijo: lo decide el instalador al arrancar (--use-free-space)
+#
+# Salida: work/takana-instalador.cpio.gz  (+ el `takana kernel kexec` que lo salta, impreso al final)
+set -eu
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+
+ARGS_INSTALADOR=""
+while [ $# -gt 0 ]; do ARGS_INSTALADOR="$ARGS_INSTALADOR $1"; shift; done
+[ -n "$ARGS_INSTALADOR" ] || { echo "uso: $0 <argumentos que se le pasarán a takana-install-partition.sh>" >&2; exit 2; }
+
+OUT="${OUT:-work/takana-instalador.cpio.gz}"
+STAGE="${STAGE:-work/.instalador-initramfs-stage}"
+IRD_TREE="${IRD_TREE:-work/.instalador-initramfs-tree}"
+KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
+[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro un bzImage de linux-metal (set KERNEL=)" >&2; exit 1; }
+
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
+MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs)  || exit 1
+SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
+MMD=$(herramienta    mtools usr/bin/mmd   mmd)      || exit 1
+MCOPY=$(herramienta  mtools usr/bin/mcopy mcopy)    || exit 1
+MDIR=$(herramienta   mtools usr/bin/mdir  mdir)     || exit 1
+CPIO=$(herramienta   libarchive usr/bin/bsdcpio cpio) || exit 1
+GZIP=$(herramienta   gzip bin/gzip gzip)            || exit 1
+
+# ── 1) el stage (rootfs a instalar + initramfs de pivote) lo arma el script validado ───────────
+echo "==> stage del sistema a instalar"
+STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
+[ -d "$STAGE/root" ] && [ -r "$STAGE/initramfs.cpio.gz" ] || { echo "el stage no quedó completo" >&2; exit 1; }
+
+# ── 2) ¿entra en la RAM de esta máquina? ───────────────────────────────────────────────────────
+# Un initramfs vive ENTERO en memoria, y el kernel no lo avisa con elegancia: se queda sin RAM al
+# desempaquetar y el arranque muere sin un mensaje que diga por qué. Medirlo antes es la diferencia
+# entre «no entra, usá el USB» y un cuelgue mudo en la máquina del usuario.
+_mib_payload=$(du -sm "$STAGE/root" | cut -f1)
+_mib_ram=$(( $(awk '/^MemTotal:/ {print $2}' /proc/meminfo) / 1024 ))
+echo "    payload ${_mib_payload} MiB · RAM de esta máquina ${_mib_ram} MiB"
+if [ "$(( _mib_payload * 100 / _mib_ram ))" -gt 40 ]; then
+    echo "instalador-initramfs: el payload es más del 40 % de la RAM ($_mib_payload de $_mib_ram MiB)." >&2
+    echo "                      En RAM van el initramfs comprimido Y su contenido desempaquetado." >&2
+    echo "                      Este camino no sirve para este rootfs: instalá desde un medio." >&2
+    exit 1
+fi
+
+# ── 3) el árbol del initramfs ──────────────────────────────────────────────────────────────────
+echo "==> árbol del initramfs-instalador"
+rm -rf "$IRD_TREE"; mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" \
+    "$IRD_TREE/instalador/lib" "$IRD_TREE/payload"
+BUSYBOX="${BUSYBOX:-$STAGE/root/bin/busybox}"
+[ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX" >&2; exit 1; }
+cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
+for a in sh mount umount mkdir echo sleep ls cat sed awk grep cut tr basename dirname readlink \
+         find sync reboot dmesg blkid setsid tee du wc head tail sort printf test sha256sum; do
+    ln -sf busybox "$IRD_TREE/bin/$a"
+done
+# Las herramientas de disco NO salen de busybox: son las SELLADAS del corpus, estáticas musl. Dentro
+# del initramfs no hay store del que resolverlas por hash, así que viajan copiadas y el instalador
+# las recibe por `TOOL_<binario>` — el override que existe exactamente para esto.
+for t in "$MKE2FS" "$SFDISK" "$MMD" "$MCOPY" "$MDIR"; do
+    cp "$t" "$IRD_TREE/bin/$(basename "$t")"; chmod 0755 "$IRD_TREE/bin/$(basename "$t")"
+done
+TAKANA_MUSL="${TAKANA_MUSL:-$ROOT/target/x86_64-unknown-linux-musl/release/takana}"
+[ -x "$TAKANA_MUSL" ] || TAKANA_MUSL="$ROOT/target/release/takana"
+cp "$TAKANA_MUSL" "$IRD_TREE/bin/takana"; chmod 0755 "$IRD_TREE/bin/takana"
+cp scripts/takana-install-partition.sh "$IRD_TREE/instalador/"
+cp scripts/lib/herramientas-desde-store.sh "$IRD_TREE/instalador/lib/"
+cp "$KERNEL" "$IRD_TREE/payload/bzImage"
+mkdir -p "$IRD_TREE/payload/stage"
+cp -a "$STAGE/root" "$IRD_TREE/payload/stage/root"
+cp "$STAGE/initramfs.cpio.gz" "$IRD_TREE/payload/stage/initramfs.cpio.gz"
+printf '%s\n' "$ARGS_INSTALADOR" > "$IRD_TREE/instalador/args"
+
+# ── 4) /init — lo que corre cuando el kernel nuevo toma el control ─────────────────────────────
+cat > "$IRD_TREE/init" <<'INIT'
+#!/bin/busybox sh
+# PID 1 del instalador. No pivota a ningún disco: instala y reinicia.
+export PATH=/bin
+mount -t proc     proc /proc 2>/dev/null || true
+mount -t sysfs    sys  /sys  2>/dev/null || true
+mount -t devtmpfs dev  /dev  2>/dev/null || true
+# ⚠ El log va a tty0 **y** al serial, por lo mismo que el initramfs de pivote: el cmdline horneado
+# deja /dev/console en ttyS0 y en un metal sin puerto serie todo esto sería invisible.
+log() { echo "$*"; for d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$d" 2>/dev/null || true; done; }
+rescate() {
+    log "TAKANA-INSTALADOR-FALLO: $*"
+    log "  bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
+    log "  shell de rescate en tty1 — probá: blkid ; dmesg | tail -30"
+    setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
+    exec sh
+}
+log "TAKANA-INSTALADOR: arrancado en RAM. El sistema viejo YA NO CORRE: su disco está libre."
+
+# Los discos tardan en enumerar (USB y NVMe sobre todo). Misma espera larga que el pivote, y por el
+# mismo motivo: en QEMU aparecen al instante y la carrera sólo se ve en metal.
+i=0
+while [ "$i" -lt 60 ]; do
+    ls /dev/nvme* /dev/sd* /dev/vd* /dev/mmcblk* >/dev/null 2>&1 && break
+    i=$(( i + 1 )); [ $(( i % 5 )) -eq 0 ] && log "TAKANA-INSTALADOR: esperando discos… ${i}s"
+    sleep 1
+done
+
+cd /instalador
+ARGS=$(cat /instalador/args)
+log "TAKANA-INSTALADOR: takana-install-partition.sh $ARGS"
+# Las herramientas viajan en /bin: el instalador las toma por TOOL_<binario> en vez de buscarlas en
+# un store que acá no existe. STAGE_LISTO dice que el árbol ya vino armado.
+export TOOL_mke2fs=/bin/mke2fs TOOL_sfdisk=/bin/sfdisk TOOL_mmd=/bin/mmd \
+       TOOL_mcopy=/bin/mcopy TOOL_mdir=/bin/mdir
+export STAGE=/payload/stage STAGE_LISTO=1 KERNEL=/payload/bzImage HAMMER=/bin/takana
+# shellcheck disable=SC2086
+if sh /instalador/takana-install-partition.sh $ARGS --yes 2>&1 | tee /dev/tty0 > /dev/ttyS0; then
+    log "TAKANA-INSTALADOR-OK: instalado. Reiniciando en 10 s (sacá el medio si lo hubiera)."
+    sync; sleep 10; reboot -f
+else
+    rescate "el instalador salió con error — NADA se reinicia solo, mirá el log de arriba"
+fi
+INIT
+chmod +x "$IRD_TREE/init"
+
+# ── 5) empaquetar ──────────────────────────────────────────────────────────────────────────────
+mkdir -p "$(dirname "$OUT")"
+( cd "$IRD_TREE" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$OUT"
+echo "==> $OUT: $(du -h "$OUT" | cut -f1)"
+echo
+echo "Para SALTAR a él sin pasar por el firmware (el userspace de esta máquina muere ACÁ):"
+echo "  sudo takana kernel kexec --probe          # ¿se puede? exit 0 = sí"
+echo "  sudo takana kernel kexec <linux-metal> --initrd $OUT \\"
+echo "       --cmdline 'console=tty0 console=ttyS0,115200' --jump"
+echo
+echo "Para probarlo SIN saltar (y sin riesgo), en QEMU:"
+echo "  qemu-system-x86_64 -m 4096 -kernel $KERNEL -initrd $OUT \\"
+echo "       -append 'console=ttyS0' -drive file=<disco.img>,format=raw -nographic"
diff --git a/scripts/install-particion-test.sh b/scripts/install-particion-test.sh
index 87e3ee88..86711926 100755
--- a/scripts/install-particion-test.sh
+++ b/scripts/install-particion-test.sh
@@ -56,8 +56,10 @@ falla(){ echo "  ✗ $*" >&2; FALLOS=$(( FALLOS + 1 )); }
 # es el escenario del punto 6. Con 1, la ESP se formatea aparte y se empalma en su offset — que es
 # la forma correcta, y la que usa install-image-efi.sh.
 ESP_SANA="${ESP_SANA:-1}"
-echo "==> fabricando $D (GPT: ESP 64M + destino 1200M + datos del usuario 500M) ESP_SANA=$ESP_SANA"
-rm -f "$D"; truncate -s 2600M "$D"
+echo "==> fabricando $D (GPT: ESP 64M + destino 1200M + datos 500M + ~3,2G SIN ASIGNAR) ESP_SANA=$ESP_SANA"
+# 5000 MiB (sparse, no ocupan en disco) para que quede un HUECO SIN ASIGNAR al final: es lo que
+# necesita el escenario de `--use-free-space`, que instala al lado en vez de encima.
+rm -f "$D"; truncate -s 5000M "$D"
 "$SFDISK" --quiet "$D" <<EOF
 label: gpt
 start=2048,    size=131072,  type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="EFI System Partition"
@@ -137,6 +139,31 @@ mount "${LOOP}p3" "$P/mnt"
     && ok "el fichero del usuario se sigue leyendo en la partición vecina" || falla "SE PERDIÓ EL FICHERO DEL USUARIO"
 umount "$P/mnt"
 
+# ── escenario 2: instalar AL LADO, en el hueco sin asignar ────────────────────────────────────
+# Es el camino al que cae la política de la raíz viva cuando la máquina no puede kexec, así que
+# tiene que estar igual de probado. Lo que se comprueba es que la tabla crece en UNA entrada y que
+# las que ya estaban no se mueven ni un sector.
+echo
+echo "==> escenario 2: --use-free-space (instalar al lado, en el hueco)"
+"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-antes2"
+_n_antes=$(wc -l < "$P/tabla-antes2")
+sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes2"
+ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \
+    ./scripts/takana-install-partition.sh --use-free-space --disk "$LOOP" --esp "${LOOP}p1" \
+    --no-nvram --yes 2>"$P/stderr2" | tail -3
+"$SFDISK" -d "$LOOP" 2>/dev/null | grep -E "^${LOOP}p" > "$P/tabla-ahora2"
+_n_ahora=$(wc -l < "$P/tabla-ahora2")
+[ "$_n_ahora" = "$(( _n_antes + 1 ))" ] && ok "la tabla creció en UNA entrada ($_n_antes → $_n_ahora)" \
+    || falla "la tabla pasó de $_n_antes a $_n_ahora entradas"
+head -n "$_n_antes" "$P/tabla-ahora2" > "$P/tabla-viejas2"
+cmp -s "$P/tabla-antes2" "$P/tabla-viejas2" && ok "las particiones que ya estaban no se movieron" \
+    || falla "SE MODIFICÓ ALGUNA ENTRADA EXISTENTE"
+[ "$(sha256sum "${LOOP}p3" | cut -d' ' -f1)" = "$(cat "$P/p3-antes2")" ] \
+    && ok "los datos del usuario siguen idénticos tras instalar al lado" || falla "LA VECINA CAMBIÓ AL INSTALAR AL LADO"
+_nueva=$(tail -1 "$P/tabla-ahora2" | cut -d' ' -f1 | tr -d ':')
+[ "$(blkid -s LABEL -o value "$_nueva" 2>/dev/null)" = "hammer-root" ] \
+    && ok "la partición nueva ($_nueva) quedó ext4 LABEL=hammer-root" || falla "la partición nueva no tiene el filesystem esperado"
+
 echo
 [ "$FALLOS" = "0" ] && { echo "VERDE: instaló sin tocar nada de lo ajeno ($LOOP)"; exit 0; }
 echo "ROJO: $FALLOS comprobación(es) fallaron" >&2; exit 1
diff --git a/scripts/lib/herramientas-desde-store.sh b/scripts/lib/herramientas-desde-store.sh
index 5cbc3c2d..33a00350 100644
--- a/scripts/lib/herramientas-desde-store.sh
+++ b/scripts/lib/herramientas-desde-store.sh
@@ -52,6 +52,18 @@ _herr_takana() {
 # herramienta <receta> <ruta-relativa-en-el-artefacto> [nombre-en-el-host]
 herramienta() {
     _hr_receta="$1"; _hr_rel="$2"; _hr_host="${3:-}"
+    # ── Override explícito: `TOOL_<binario>=/ruta` ────────────────────────────────────────────────
+    # Existe para los entornos donde NO HAY STORE y no puede haberlo: dentro del initramfs de un
+    # instalador, por ejemplo, que lleva los binarios adentro porque el disco todavía no está
+    # formateado. Ahí `herramienta` no puede calcular un hash ni leer el store, y caer al host es
+    # caer a un host que tampoco existe. El override es explícito y se anuncia igual que el resto:
+    # quien lo pone sabe lo que hace, y el log lo dice para que no se confunda con un sellado.
+    _hr_bin="${_hr_rel##*/}"
+    eval "_hr_ovr=\${TOOL_$(printf '%s' "$_hr_bin" | tr -c 'A-Za-z0-9_' '_'):-}"
+    if [ -n "${_hr_ovr:-}" ] && [ -x "$_hr_ovr" ]; then
+        echo "   · $_hr_bin ← $_hr_ovr (TOOL_ explícito, sin store)" >&2
+        echo "$_hr_ovr"; return 0
+    fi
     _hr_store="$(_herr_store)"; _hr_tk="$(_herr_takana)"
     _hr_hash=$("$_hr_tk" --store "$_hr_store" hash "recipes/$_hr_receta.toml" 2>/dev/null \
                | tail -1 | sed 's/^b3://')
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index 9df70323..a5bb4e0c 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -7,6 +7,8 @@
 # presupone lo contrario: **el disco es de otro y takana llega de visita**.
 #
 #   - NO escribe la tabla de particiones. Ni una vez. No invoca fdisk/sfdisk/parted.
+#     (La única excepción es `--use-free-space`, que AÑADE una entrada en un hueco sin asignar y no
+#     modifica ninguna de las existentes. Se dice aparte porque es la única vez que la tabla cambia.)
 #   - Formatea EXACTAMENTE una partición: la que se le nombra.
 #   - Reusa la ESP que ya existe, y escribe sólo dentro de `\EFI\takana\` — la ruta de VENDOR, que
 #     es nuestra. `\EFI\BOOT\BOOTX64.EFI` es tierra de nadie (Windows la repuebla) y **no se pisa**
@@ -26,6 +28,10 @@
 #
 # Opciones:
 #   --root-part <dev|fichero>  DESTINO. Se formatea ext4 con LABEL=hammer-root. Lo ÚNICO destructivo.
+#   --use-free-space           no formatear nada existente: crear la partición en el hueco sin
+#                              asignar más grande de `--disk` e instalar ahí. Al lado, no encima.
+#   --on-active-root <modo>    qué hacer si `--root-part` ES la raíz viva (auto|kexec|free-space|abort).
+#                              `auto` (def) = intentar kexec y, si esta máquina no puede, espacio libre.
 #   --esp <dev|fichero>        ESP existente a poblar (FAT). Sin ella no hay arranque.
 #   --disk <dev> --esp-part N  para la entrada NVRAM; si no se dan, se deducen del `--esp`.
 #   --claim-fallback           además, escribir \EFI\BOOT\BOOTX64.EFI (pisa lo que haya ahí).
@@ -36,6 +42,7 @@ set -eu
 ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 
 DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
+USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"
 while [ $# -gt 0 ]; do
     case "$1" in
         --root-part) DEST="${2:?}"; shift 2 ;;
@@ -44,18 +51,141 @@ while [ $# -gt 0 ]; do
         --esp-part)  ESP_PART="${2:?}"; shift 2 ;;
         --claim-fallback) CLAIM_FALLBACK=1; shift ;;
         --no-nvram)  NO_NVRAM=1; shift ;;
+        --use-free-space) USE_FREE=1; shift ;;
+        --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
         --yes|-y)    ASSUME_YES=1; shift ;;
         -h|--help)   sed -n '2,40p' "$0"; exit 0 ;;
         *) echo "opción desconocida: $1 (--help)" >&2; exit 2 ;;
     esac
 done
-[ -n "$DEST" ] || { echo "falta --root-part (--help)" >&2; exit 2; }
+[ -n "$DEST" ] || [ -n "$USE_FREE" ] || { echo "falta --root-part (o --use-free-space) — ver --help" >&2; exit 2; }
+case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root: auto|kexec|free-space|abort" >&2; exit 2 ;; esac
+
+# ── Las herramientas, ANTES que nada ───────────────────────────────────────────────────────────
+# Se resuelven acá arriba y no junto a su uso porque `--use-free-space` necesita `sfdisk` durante
+# las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
+# inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
+MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
+SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
+# Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
+# device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
+# si alguna partición del disco está montada — por eso se intentan los dos, en ese orden. Los dos
+# salen del `util-linux` sellado: en el anfitrión musl no existe ninguno (ni `partprobe`).
+PARTX=$(herramienta util-linux usr/sbin/partx partx 2>/dev/null || echo "")
+BLOCKDEV=$(herramienta util-linux sbin/blockdev blockdev 2>/dev/null || echo "")
+MMD=$(herramienta    mtools usr/bin/mmd   mmd)     || exit 1
+MCOPY=$(herramienta  mtools usr/bin/mcopy mcopy)   || exit 1
+MDIR=$(herramienta   mtools usr/bin/mdir  mdir)    || exit 1
+KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
+[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
+HAMMER="${HAMMER:-$ROOT/target/release/takana}"
 
 # ── LAS GUARDAS VAN PRIMERO, Y SON EL PUNTO DE ESTE SCRIPT ─────────────────────────────────────
 # Un instalador que borra de más falla del lado silencioso: el usuario se entera cuando reinicia y
 # le falta su /home. Cada guarda de abajo existe porque su ausencia es irreversible.
 es_bloque() { [ -b "$1" ]; }
 nombre_kernel() { basename "$(readlink -f "$1")"; }
+disco_de() { echo "/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$1")/..")")"; }
+
+# ── releer_tabla <disco> — que el KERNEL se entere de la entrada nueva ────────────────────────
+# Medido el 2026-09-23 en el banco: `sfdisk --append` escribió la entrada perfectamente y
+# `/dev/loop0p4` NO APARECIÓ. El script lo detectó y abortó —bien— pero el motivo estaba fuera de
+# él: en el anfitrión musl no existen `partprobe`, `partx` ni `blockdev`, así que los tres intentos
+# de releer la tabla eran no-ops silenciosos. La tabla en disco y la que el kernel tiene en memoria
+# son dos cosas distintas, y nada avisa cuando divergen.
+# ⚠ Y el comando es `partx -u`, NO `partx -a`: medido en el banco el mismo día. `-a` («añadir»)
+# intenta registrar TODAS las particiones y falla entero —«error adding partitions 1-4»— cuando las
+# viejas ya están, que es siempre en este flujo. `-u` («actualizar») añade lo que falte y deja en paz
+# lo que ya hay. El nombre de la opción sugiere justo lo contrario de lo que sirve acá.
+releer_tabla() {
+    [ -n "${PARTX:-}" ] && "$PARTX" -u "$1" >/dev/null 2>&1 && return 0
+    [ -n "${PARTX:-}" ] && "$PARTX" -a "$1" >/dev/null 2>&1 && return 0
+    [ -n "${BLOCKDEV:-}" ] && "$BLOCKDEV" --rereadpt "$1" >/dev/null 2>&1 && return 0
+    partprobe "$1" 2>/dev/null || true
+}
+
+# ── mib_necesarios — cuánto pedirle al hueco ──────────────────────────────────────────────────
+# El rootfs medido + 40 %, con piso de 2 GiB. El margen no es superstición: `mke2fs` se queda con
+# ~1,5 % en metadatos y el sistema instalado tiene que poder escribir algo antes de que alguien lo
+# agrande. Un destino que entra JUSTO produce un sistema que arranca y se llena a la semana.
+mib_necesarios() {
+    _mn_r="${ROOTFS:-}"
+    [ -z "$_mn_r" ] && _mn_r=$(ls -dt store/*-product-rootfs /store/*-product-rootfs 2>/dev/null | head -1)
+    _mn_m=$(du -sm "$_mn_r" 2>/dev/null | cut -f1)
+    [ -z "${_mn_m:-}" ] && _mn_m=1024
+    _mn_m=$(( _mn_m * 14 / 10 ))
+    [ "$_mn_m" -lt 2048 ] && _mn_m=2048
+    echo "$_mn_m"
+}
+
+# ── crear_en_hueco <disco> <mib> — la ÚNICA vez que este script escribe la tabla ───────────────
+# `sfdisk --append` añade una entrada y **no reescribe las existentes**: es la diferencia entre
+# «instalar al lado» y el `o` de `takana-live-install.sh`, que empieza por tirar la tabla entera.
+# Aun así es la operación más delicada de acá, así que: se elige el hueco MÁS GRANDE, se dice cuál
+# es con sus sectores a la vista, y se pide confirmación aparte de la del formateo.
+crear_en_hueco() {
+    _ch_disco="$1"; _ch_mib="$2"
+    _ch_libre=$("$SFDISK" -F "$_ch_disco" 2>/dev/null | awk 'NF==4 && $1 ~ /^[0-9]+$/ {if ($3+0 > m) {m=$3+0; s=$1; e=$2}} END {if (m>0) print s, e, m}')
+    [ -n "$_ch_libre" ] || { echo "takana-install-partition: $_ch_disco no tiene espacio SIN ASIGNAR" >&2; return 1; }
+    _ch_start=$(echo "$_ch_libre" | cut -d' ' -f1)
+    _ch_sect=$(echo  "$_ch_libre" | cut -d' ' -f3)
+    _ch_libre_mib=$(( _ch_sect / 2048 ))
+    echo "    hueco más grande: ${_ch_libre_mib} MiB desde el sector ${_ch_start}" >&2
+    [ "$_ch_libre_mib" -ge "$_ch_mib" ] || {
+        echo "takana-install-partition: el hueco da ${_ch_libre_mib} MiB y hacen falta ${_ch_mib} MiB." >&2
+        echo "                          Nada que hacer sin encoger algo, y encoger pide otro medio." >&2; return 1; }
+    _ch_size=$(( _ch_mib * 2048 ))
+    if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
+        printf "    voy a AÑADIR una partición de %s MiB a la tabla de %s (las existentes no se tocan) [escribí si] " "$_ch_mib" "$_ch_disco" >&2
+        read -r _ch_r; [ "$_ch_r" = "si" ] || { echo "    cancelado." >&2; return 1; }
+    fi
+    # La lista de ANTES, para saber después cuál es la nueva por DIFERENCIA y no por su nombre.
+    # ⚠ El primer intento buscaba la línea por `start=<sector>` y nunca la encontraba: `sfdisk -d`
+    # **alinea los campos con espacios** (`start=     3614720`), así que el patrón sin espacios no
+    # casa jamás. El síntoma era idéntico al de un device que no aparece — y mandó a buscar el fallo
+    # en `partx` y en udev, que estaban bien. Comparar conjuntos no depende del formato.
+    _ch_lista_antes=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
+    _ch_antes=$(printf '%s\n' "$_ch_lista_antes" | grep -c . || true)
+    echo "start=$_ch_start, size=$_ch_size, type=linux, name=\"takana-root\"" \
+        | "$SFDISK" --append --quiet "$_ch_disco" >/dev/null 2>&1 \
+        || { echo "takana-install-partition: sfdisk --append falló sobre $_ch_disco" >&2; return 1; }
+    # El device nuevo tarda en aparecer (udev, o `partx` en un loop): esperarlo es obligatorio, y el
+    # nombre se LEE de la tabla en vez de construirlo a mano — `nvme0n1p4` y `sda4` no se escriben igual.
+    releer_tabla "$_ch_disco"
+    _ch_nuevo=""
+    _ch_i=0
+    while [ "$_ch_i" -lt 15 ]; do
+        _ch_lista_post=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
+        for _ch_d in $_ch_lista_post; do
+            case " $(printf '%s ' $_ch_lista_antes)" in
+                *" $_ch_d "*) ;;
+                *) _ch_nuevo="$_ch_d" ;;
+            esac
+        done
+        [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] && break
+        _ch_nuevo=""
+        _ch_i=$(( _ch_i + 1 )); sleep 1
+        releer_tabla "$_ch_disco"
+    done
+    [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] || {
+        echo "takana-install-partition: la partición se añadió pero su device no apareció (¿hace falta un partprobe?)" >&2; return 1; }
+    _ch_despues=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}' | grep -c . || true)
+    [ "$_ch_despues" = "$(( _ch_antes + 1 ))" ] || {
+        echo "takana-install-partition: la tabla pasó de $_ch_antes a $_ch_despues entradas — esperaba UNA más" >&2; return 1; }
+    echo "$_ch_nuevo"
+}
+
+# `--use-free-space` sin `--root-part`: el destino todavía no existe, se crea en un hueco. Esto es
+# «instalar al lado», y es el camino que la política de la raíz viva usa cuando no hay kexec.
+if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
+    _ufs_disco="${DISK:-}"
+    if [ -z "$_ufs_disco" ] && [ -n "$ESP" ] && [ -b "$ESP" ]; then _ufs_disco=$(disco_de "$ESP"); fi
+    [ -n "$_ufs_disco" ] || { echo "takana-install-partition: --use-free-space necesita --disk (o un --esp real)" >&2; exit 2; }
+    echo "==> buscando espacio SIN ASIGNAR en $_ufs_disco"
+    DEST=$(crear_en_hueco "$_ufs_disco" "$(mib_necesarios)") || exit 1
+    echo "    partición creada: $DEST"
+fi
 
 if es_bloque "$DEST"; then
     _n=$(nombre_kernel "$DEST")
@@ -72,9 +202,9 @@ if es_bloque "$DEST"; then
     # 3) NO PUEDE SER LA RAÍZ VIVA. Es el caso que el usuario pregunta siempre: «¿puedo instalar
     #    encima del Linux que estoy usando?». No desde él: hay que saltar antes a otro kernel/medio.
     _rootdev=$(awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null || true)
-    [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ] && {
-        echo "takana-install-partition: $DEST es la RAÍZ VIVA de este sistema. No se puede reemplazar" >&2
-        echo "                          el sistema que está corriendo desde adentro." >&2; exit 1; }
+    if [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
+        politica_raiz_viva      # kexec → espacio libre → abortar, según --on-active-root
+    fi
     # 4) NO PUEDE SER LA ESP que vamos a reusar.
     [ -n "$ESP" ] && [ "$(readlink -f "$DEST")" = "$(readlink -f "$ESP")" ] && {
         echo "takana-install-partition: --root-part y --esp son la MISMA partición" >&2; exit 1; }
@@ -82,8 +212,79 @@ else
     [ -f "$DEST" ] || { echo "takana-install-partition: $DEST no existe (ni bloque ni fichero)" >&2; exit 1; }
 fi
 
+# ── politica_raiz_viva — el «saltito»: qué hacer si el destino ES el sistema que corre ────────
+# La pregunta que todo el mundo hace es «¿puedo poner takana encima del Linux que estoy usando, sin
+# USB?». La respuesta honesta tiene tres partes y este bloque las implementa en orden:
+#
+#   1. Desde adentro, NO. Un filesystem montado y en uso no se formatea; eso es el kernel, no una
+#      política nuestra.
+#   2. Pero se puede SALIR de adentro sin reiniciar por el firmware: `kexec` salta a otro kernel con
+#      un instalador en RAM, y en ese salto el sistema viejo deja de correr ⇒ su disco queda libre.
+#      Es el ADR 0017 §1, y el syscall ya estaba implementado (`kexec_file_load`, sin kexec-tools).
+#   3. Y si esta máquina no puede saltar —kernel sin `CONFIG_KEXEC_FILE`, o lockdown por Secure
+#      Boot— no se fuerza nada: se instala AL LADO, en espacio sin asignar, y el usuario elige en el
+#      arranque. Peor camino, cero riesgo.
+#
+# `--on-active-root` permite fijar uno de los tres a mano; `auto` es la política de arriba.
+politica_raiz_viva() {
+    echo "⚠ $DEST es la RAÍZ VIVA: el sistema que está corriendo ahora mismo."
+    echo "  Un filesystem montado y en uso no se puede formatear desde adentro."
+    case "$ON_ACTIVE" in
+        abort)
+            echo "  --on-active-root=abort ⇒ no hago nada más." >&2; exit 1 ;;
+    esac
+    _prv_kexec=no
+    if [ "$ON_ACTIVE" = auto ] || [ "$ON_ACTIVE" = kexec ]; then
+        echo "==> ¿puede esta máquina saltar a otro kernel sin pasar por el firmware?"
+        if _prv_out=$("$HAMMER" kernel kexec --probe 2>&1); then _prv_kexec=si; fi
+        printf '%s\n' "$_prv_out" | sed 's/^/    /'
+    fi
+    if [ "$_prv_kexec" = si ]; then
+        camino_kexec    # no vuelve: salta
+        exit 0
+    fi
+    if [ "$ON_ACTIVE" = kexec ]; then
+        echo "  --on-active-root=kexec y esta máquina no puede saltar (motivos arriba)." >&2; exit 1
+    fi
+    echo "==> sin kexec ⇒ INSTALO AL LADO, en espacio sin asignar (la raíz viva no se toca)"
+    _prv_disco=$(disco_de "$DEST")
+    DEST=$(crear_en_hueco "$_prv_disco" "$(mib_necesarios)") || {
+        echo "takana-install-partition: no hay hueco donde instalar al lado, y sin kexec no hay otra" >&2
+        echo "                          desde este sistema. Queda el camino del medio externo (USB)." >&2
+        exit 1; }
+    echo "    destino nuevo: $DEST (la raíz viva sigue intacta)"
+}
+
+# ── camino_kexec — armar el instalador en RAM y saltar. NO VUELVE ─────────────────────────────
+camino_kexec() {
+    echo "==> camino KEXEC: el instalador viaja en RAM y el sistema viejo muere en el salto"
+    _ck_args="--root-part $DEST"
+    [ -n "$ESP" ] && _ck_args="$_ck_args --esp $ESP"
+    [ -n "$CLAIM_FALLBACK" ] && _ck_args="$_ck_args --claim-fallback"
+    # ⚠ `--on-active-root abort` ADENTRO, y no es decorativo: si por lo que sea el destino siguiera
+    # siendo la raíz viva del sistema nuevo, `auto` volvería a intentar un kexec y tendríamos una
+    # máquina saltando a sí misma en bucle. Abortar ahí es un fallo legible.
+    _ck_args="$_ck_args --on-active-root abort"
+    _ck_ird="${IRD_INSTALADOR:-work/takana-instalador.cpio.gz}"
+    # shellcheck disable=SC2086
+    OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh $_ck_args || {
+        echo "takana-install-partition: no pude armar el instalador en RAM" >&2; exit 1; }
+    _ck_art=$(dirname "$(dirname "$KERNEL")")     # …/<hash>-linux-metal/boot/bzImage → el artefacto
+    echo
+    echo "  kernel:  $_ck_art"
+    echo "  initrd:  $_ck_ird ($(du -h "$_ck_ird" | cut -f1))"
+    echo "  destino: $DEST"
+    if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
+        printf "  ⚡ ESTE COMANDO MATA EL SISTEMA QUE ESTÁS USANDO y arranca el instalador. [escribí saltar] "
+        read -r _ck_r; [ "$_ck_r" = "saltar" ] || { echo "  cancelado — no se saltó y no se tocó nada."; exit 1; }
+    fi
+    "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" \
+        --cmdline "console=tty0 console=ttyS0,115200" --jump
+    echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
+    exit 1
+}
+
 # ── El terreno, DICHO EN VOZ ALTA antes de tocar nada (ADR 0018 §4) ───────────────────────────
-HAMMER="${HAMMER:-$ROOT/target/release/takana}"
 echo "==> destino: $DEST   ($( [ -b "$DEST" ] && echo 'partición real' || echo 'fichero de prueba'))"
 if [ -b "$DEST" ] && command -v blkid >/dev/null 2>&1; then
     echo "    lo que hay HOY ahí (se va a PERDER):"
@@ -102,19 +303,18 @@ fi
 # ── 1) Los árboles (initramfs de pivote + rootfs con su /sbin/init) los arma el script VALIDADO ──
 # No se copian acá: se PIDEN. Ver el comentario de `STAGE_ONLY` en install-image-efi.sh.
 STAGE="${STAGE:-work/.install-part-stage}"
-echo "==> armando árboles (STAGE_ONLY sobre install-image-efi.sh)"
-STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
+# `STAGE_LISTO=1` — el árbol ya está armado y no hay que (ni se puede) rearmarlo. Es lo que usa el
+# instalador cuando corre DENTRO de un initramfs: allí no hay repo, ni store, ni `install-image-efi.sh`;
+# el stage viajó horneado en el propio initramfs. Se comprueba que esté completo, no se confía.
+if [ -n "${STAGE_LISTO:-}" ]; then
+    echo "==> stage ya armado: $STAGE (STAGE_LISTO=1)"
+else
+    echo "==> armando árboles (STAGE_ONLY sobre install-image-efi.sh)"
+    STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
+fi
 ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
 [ -d "$ROOT_TREE" ] && [ -r "$IRD" ] || { echo "takana-install-partition: el stage no quedó completo" >&2; exit 1; }
 
-. "$(dirname "$0")/lib/herramientas-desde-store.sh"
-MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
-MMD=$(herramienta    mtools usr/bin/mmd   mmd)     || exit 1
-MCOPY=$(herramienta  mtools usr/bin/mcopy mcopy)   || exit 1
-MDIR=$(herramienta   mtools usr/bin/mdir  mdir)    || exit 1
-KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
-[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
-
 # ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
 # `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
 # montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
@@ -203,5 +403,10 @@ else
     echo "    · NVRAM no tocada (--no-nvram, sin efivarfs, o destino de prueba)"
 fi
 
-echo "✓ takana instalada en $DEST. La tabla de particiones NO se tocó y las demás particiones siguen como estaban."
+if [ -n "$USE_FREE" ]; then
+    echo "✓ takana instalada en $DEST, una partición NUEVA creada en espacio sin asignar."
+    echo "  La tabla creció en una entrada; ninguna de las que ya estaban se modificó."
+else
+    echo "✓ takana instalada en $DEST. La tabla de particiones NO se tocó y las demás particiones siguen como estaban."
+fi
 echo "  Comprobalo antes de reiniciar:  lsblk -o NAME,SIZE,FSTYPE,LABEL  ·  $HAMMER boot entry list"