tawasuyu

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 1181Shell 82Rust 62Markdown 521515 Definiciones

Etapa F refinamientos: uninstall poda dirs vacíos + `install --require-signed`

Dos bordes ásperos de la paquetería:
- uninstall ahora retira los directorios que quedaron VACÍOS por el borrado (rmdir de abajo
  arriba; remove_dir sólo borra dirs vacíos ⇒ se detiene solo al toparse con contenido de otro
  paquete). Antes dejaba /usr/bin, etc. huérfanos.
- `install --require-signed`: modo estricto que ABORTA si el release no está firmado por una
  clave confiada (Unsigned o UnknownKey ⇒ error). No basta con que el .swm reproduzca: exige
  autoría verificada del catálogo. Default off (no rompe flujos sin firma).

Validado E2E: uninstall bwrap poda 4 dirs; --require-signed aborta sin firma y procede con
release trusted. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

f94ef011392b6ffae1e94967abfd015be4299f0b · por sergio · (hace 4 meses) · padre 910db267e · ver el árbol en este commit

Delta semántico · 4

crates/hammer-cli/src/main.rs

2 archivos · +46 −6

diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index 478b6cd9..6deef097 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -230,6 +230,10 @@ enum Cmd {
         /// Base de datos de instalados donde registrar el paquete (para `uninstall`).
         #[arg(long, default_value = hammer_core::installed::DEFAULT_DB)]
         db: PathBuf,
+        /// Modo estricto: abortar si el release del repo no está firmado por una clave confiada
+        /// (no basta con que el `.swm` reproduzca — exige autoría del catálogo verificada).
+        #[arg(long)]
+        require_signed: bool,
     },
     /// [Etapa F] Desinstala un paquete: borra los ficheros que registró (refcount: respeta los
     /// que otro paquete instalado también aporta) y lo quita de la DB de instalados.
@@ -791,6 +795,7 @@ fn main() -> anyhow::Result<()> {
             trust,
             state_root,
             db,
+            require_signed,
         } => {
             run_install(
                 &cli.store,
@@ -802,6 +807,7 @@ fn main() -> anyhow::Result<()> {
                 trust.as_deref(),
                 state_root.as_deref(),
                 &db,
+                require_signed,
             )?;
         }
         Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?,
@@ -1426,6 +1432,7 @@ fn run_install(
     trust: Option<&std::path::Path>,
     state_root: Option<&std::path::Path>,
     db_path: &std::path::Path,
+    require_signed: bool,
 ) -> anyhow::Result<()> {
     let source = RepoSource::parse(repo);
     let index = source.read_index()?;
@@ -1439,12 +1446,20 @@ fn run_install(
         match index.verify_signature(&store) {
             hammer_core::SigStatus::Trusted { by } => eprintln!("release: trusted (by {by})"),
             hammer_core::SigStatus::UnknownKey { by } => {
-                eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada")
+                eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada");
+                if require_signed {
+                    anyhow::bail!("--require-signed: el release está firmado por '{by}', clave no confiada en {}", trust_dir.display());
+                }
             }
             hammer_core::SigStatus::BadSig { by, reason } => {
                 anyhow::bail!("release: BAD-SIG (by {by}): {reason} — el índice fue manipulado")
             }
-            hammer_core::SigStatus::Unsigned => eprintln!("release: sin firmar"),
+            hammer_core::SigStatus::Unsigned => {
+                eprintln!("release: sin firmar");
+                if require_signed {
+                    anyhow::bail!("--require-signed: el repo no tiene firma de release (firmalo con `hammer repo sign`)");
+                }
+            }
         }
     }
 
@@ -1592,6 +1607,7 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
     };
 
     let (mut removed, mut kept) = (0usize, 0usize);
+    let mut touched_dirs: Vec<PathBuf> = Vec::new();
     for f in &pkg.files {
         // Refcount: no borramos un fichero que otro paquete instalado también aporta.
         if idb.owned_by_others(name, f) {
@@ -1599,17 +1615,40 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
             continue;
         }
         match std::fs::remove_file(f) {
-            Ok(()) => removed += 1,
+            Ok(()) => {
+                removed += 1;
+                if let Some(parent) = std::path::Path::new(f).parent() {
+                    touched_dirs.push(parent.to_path_buf());
+                }
+            }
             Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} // ya no estaba
             Err(e) => eprintln!("  aviso: no pude borrar {f}: {e}"),
         }
     }
+    // Recoge los directorios que quedaron vacíos por el borrado (y sus padres, hacia arriba):
+    // `remove_dir` sólo borra un dir VACÍO ⇒ se detiene solo al toparse con uno que aún tiene
+    // contenido de otros paquetes. Procesamos de más profundo a menos para vaciar de abajo arriba.
+    let mut pruned = 0usize;
+    touched_dirs.sort_by_key(|d| std::cmp::Reverse(d.components().count()));
+    touched_dirs.dedup();
+    for dir in touched_dirs {
+        let mut cur = dir;
+        while std::fs::remove_dir(&cur).is_ok() {
+            pruned += 1;
+            match cur.parent() {
+                Some(p) if !p.as_os_str().is_empty() => cur = p.to_path_buf(),
+                _ => break,
+            }
+        }
+    }
+
     idb.remove(name);
     idb.save(db_path)?;
     eprintln!(
-        "desinstalado {name} {} — {removed} fichero(s) borrado(s){}",
+        "desinstalado {name} {} — {removed} fichero(s) borrado(s){}{}",
         pkg.version,
-        if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() }
+        if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() },
+        if pruned > 0 { format!(", {pruned} dir(s) vacío(s) retirado(s)") } else { String::new() },
     );
     Ok(())
 }
diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md
index 2d8f1741..78159c01 100644
--- a/docs/06-swm-format.md
+++ b/docs/06-swm-format.md
@@ -145,7 +145,8 @@ CLI:
   un binario ajeno. Registra el paquete en la DB de instalados (`--db`). `--repo` admite un
   directorio local **o una URL HTTP(S)** (el repo son ficheros estáticos: `index.json` + los
   `.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual).
-  `--prefix`/`--skip-source-patch` para staging y dry-run de schema.
+  `--prefix`/`--skip-source-patch` para staging y dry-run de schema. `--require-signed` exige que
+  el release esté firmado por una clave confiada (modo estricto: no basta con reproducir).
 - `hammer repo list [--repo DIR]` — lista el catálogo (`<repo>/index.json`).
 - `hammer repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras
   publicar (cada `pack --repo` invalida la firma del release).