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 Sutichaykuna

hammer-mirror: mirror del store content-addressed (Etapa E3, primer corte)

Nuevo crate hammer-mirror + CLI `hammer mirror push|pull|status <remote>`. El /store es un
CAS (dir `<hash>-<name>`, el hash ES la dirección); un mirror = ese CAS replicado entre
máquinas + resolución por hash. La integridad se ancla en of_tree (content-hash BLAKE3 del
árbol): el receptor RECOMPUTA of_tree sobre lo copiado y exige que case el del índice ANTES
de sellar (Store::seal, atómico + read-only en un staging del mismo FS) ⇒ una transferencia
corrupta/manipulada se RECHAZA, no se instala.

- index(store): enumera dirs `<64hex>-<name>` (filtra bootstrap.json/.bootstrap-tmp), of_tree
  cada uno. diff(src,dst): faltantes en cada lado + conflictos (mismo dir, otro contenido).
- sync(src,dst): copia los que faltan (verificados), idempotente, no sobrescribe conflictos.
  push = sync(local,remoto); pull = sync(remoto,local).
- Transporte = filesystem (el remoto es una ruta a otro store, p.ej. sshfs); SSH/red queda
  como envoltorio ortogonal posterior. Endurecimiento: anclar of_tree a una raíz firmada
  (bootstrap.json / atestación D) vs un origen plenamente malicioso.

5 tests unitarios (forma de dir, índice, sync idempotente, rechazo de transferencia corrupta,
conflicto-no-sobrescribe) + validado E2E con el CLI sobre artefactos reales (push/pull/status,
idempotencia, bytes idénticos, tamper→conflicto, conflicto no sobrescrito exit≠0).

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

b9d317085972d8a50d2af80623cf48b4704a8e55 · ruwaq sergio · (4 killa ñawpaq) · tayta f7ea593e2 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 26

crates/hammer-cli/src/main.rs

crates/hammer-mirror/src/lib.rs

6 qillqakuna · +473 −2

diff --git a/Cargo.toml b/Cargo.toml
index d1f99bc3..5f6f8ccb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
     "crates/hammer-bootstrap",
     "crates/hammer-overlay",
     "crates/hammer-journal",
+    "crates/hammer-mirror",
     "crates/hammer-agent",
     "crates/hammer-cli",
     "crates/hammerd",
@@ -25,6 +26,7 @@ hammer-build = { path = "crates/hammer-build" }
 hammer-bootstrap = { path = "crates/hammer-bootstrap" }
 hammer-overlay = { path = "crates/hammer-overlay" }
 hammer-journal = { path = "crates/hammer-journal" }
+hammer-mirror = { path = "crates/hammer-mirror" }
 hammer-agent = { path = "crates/hammer-agent" }
 
 # shared third-party deps, pinned at the workspace level
diff --git a/crates/hammer-cli/Cargo.toml b/crates/hammer-cli/Cargo.toml
index af7aa368..ca789a34 100644
--- a/crates/hammer-cli/Cargo.toml
+++ b/crates/hammer-cli/Cargo.toml
@@ -21,6 +21,7 @@ llm-claude = ["hammer-agent/llm-claude"]
 hammer-core.workspace = true
 hammer-build.workspace = true
 hammer-bootstrap.workspace = true
+hammer-mirror.workspace = true
 hammer-overlay.workspace = true
 hammer-journal.workspace = true
 hammer-agent.workspace = true
diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index c0fed1ba..ebf14d3a 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -239,6 +239,31 @@ enum Cmd {
         #[command(subcommand)]
         sub: BootstrapCmd,
     },
+    /// [Etapa E3] Mirror del store content-addressed: replica artefactos por hash entre máquinas,
+    /// verificando `of_tree` (BLAKE3) en recepción. `--store` es el store local.
+    Mirror {
+        #[command(subcommand)]
+        sub: MirrorCmd,
+    },
+}
+
+#[derive(Subcommand)]
+enum MirrorCmd {
+    /// Replica al store REMOTO los artefactos que le faltan (verificados antes de sellar).
+    Push {
+        /// Ruta del store remoto destino (puede ser un montaje de red/sshfs).
+        remote: String,
+    },
+    /// Trae del store REMOTO los artefactos que le faltan al local (verificados antes de sellar).
+    Pull {
+        /// Ruta del store remoto origen.
+        remote: String,
+    },
+    /// Muestra el diff entre el store local y uno remoto (qué falta en cada lado, conflictos). No copia.
+    Status {
+        /// Ruta del store remoto a comparar.
+        remote: String,
+    },
 }
 
 #[derive(Subcommand)]
@@ -742,10 +767,61 @@ fn main() -> anyhow::Result<()> {
                 println!("{}", serde_json::to_string_pretty(&manifest)?);
             }
         },
+        Cmd::Mirror { sub } => {
+            let local = std::path::PathBuf::from(&cli.store);
+            match sub {
+                MirrorCmd::Push { remote } => {
+                    let remote = std::path::PathBuf::from(&remote);
+                    let r = hammer_mirror::sync(&local, &remote)?;
+                    print_sync("push", &r);
+                    if !r.ok() {
+                        anyhow::bail!("mirror push incompleto: {} fallo(s), {} conflicto(s)", r.failed.len(), r.conflicts.len());
+                    }
+                }
+                MirrorCmd::Pull { remote } => {
+                    let remote = std::path::PathBuf::from(&remote);
+                    let r = hammer_mirror::sync(&remote, &local)?;
+                    print_sync("pull", &r);
+                    if !r.ok() {
+                        anyhow::bail!("mirror pull incompleto: {} fallo(s), {} conflicto(s)", r.failed.len(), r.conflicts.len());
+                    }
+                }
+                MirrorCmd::Status { remote } => {
+                    let remote = std::path::PathBuf::from(&remote);
+                    let (missing_remote, missing_local, conflicts) = hammer_mirror::diff(&local, &remote)?;
+                    println!("faltan en el remoto (push los lleva): {}", missing_remote.len());
+                    for a in &missing_remote { println!("  → {}", a.dir); }
+                    println!("faltan en el local (pull los trae):   {}", missing_local.len());
+                    for a in &missing_local { println!("  ← {}", a.dir); }
+                    println!("conflictos (mismo hash, otro contenido): {}", conflicts.len());
+                    for d in &conflicts { println!("  ✗ {d}"); }
+                }
+            }
+        }
     }
     Ok(())
 }
 
+/// Imprime el reporte de un `mirror push|pull`.
+fn print_sync(verbo: &str, r: &hammer_mirror::SyncReport) {
+    for d in &r.copied {
+        println!("  ✓ {d}");
+    }
+    for (d, why) in &r.failed {
+        println!("  ✗ {d}: {why}");
+    }
+    for d in &r.conflicts {
+        println!("  ⚠ conflicto {d} (no sobrescrito)");
+    }
+    println!(
+        "mirror {verbo}: {} copiados, {} ya presentes, {} conflictos, {} fallos",
+        r.copied.len(),
+        r.skipped_present,
+        r.conflicts.len(),
+        r.failed.len()
+    );
+}
+
 /// Lee y deserializa un `.swm` desde disco; falla con mensaje útil si no parsea.
 fn load_swm(file: &str) -> anyhow::Result<hammer_core::Swm> {
     let yaml = std::fs::read_to_string(file)
diff --git a/crates/hammer-mirror/Cargo.toml b/crates/hammer-mirror/Cargo.toml
new file mode 100644
index 00000000..b017236e
--- /dev/null
+++ b/crates/hammer-mirror/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "hammer-mirror"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
+description = "Mirror del store content-addressed de hammer (E3): replica artefactos por hash, verifica of_tree en recepción."
+
+[dependencies]
+hammer-core.workspace = true
+anyhow.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+
+[dev-dependencies]
+tempfile.workspace = true
diff --git a/crates/hammer-mirror/src/lib.rs b/crates/hammer-mirror/src/lib.rs
new file mode 100644
index 00000000..4416e289
--- /dev/null
+++ b/crates/hammer-mirror/src/lib.rs
@@ -0,0 +1,364 @@
+//! **E3 — mirror del store content-addressed** ([SDD 13 §E3](../../docs/13-release-engineering.md)).
+//!
+//! El `/store` de hammer es un CAS: cada artefacto vive en un directorio `<hash>-<name>` y *el hash es
+//! la dirección*. Un mirror, entonces, es ese CAS **replicado** entre dos máquinas + **resolución por
+//! hash**. Esta capa replica los artefactos que le faltan al destino y **ancla la integridad en
+//! `of_tree`** (el content-hash BLAKE3 del árbol, [`ArtifactHash::of_tree`]): el receptor **recomputa**
+//! `of_tree` sobre lo que copió y **exige** que case el del índice **antes** de sellar. Una transferencia
+//! corrupta (o un origen que sirvió bytes que no casan el hash anunciado) se **rechaza**, no se instala.
+//!
+//! El transporte de esta primera entrega es **sistema de ficheros**: el "remoto" es una ruta a otro
+//! store (que puede ser un montaje de red, sshfs, etc.). El transporte sobre SSH/red —que el producto ya
+//! expone vía sshd— es un envoltorio ortogonal posterior; la lógica de CAS (índice, diff, copia
+//! verificada, sellado atómico) vive aquí y es agnóstica del medio.
+//!
+//! **Modelo de confianza:** la verificación `of_tree` detecta corrupción de transporte y que el origen
+//! sirva un árbol que no case el hash que anuncia. Anclar ese hash a una raíz de confianza firmada
+//! (el log de transparencia `bootstrap.json` / la atestación de Etapa D) es la capa de endurecimiento
+//! posterior: hoy el invariante CAS que se *fuerza* es "el contenido recibido HASHEA a lo que el índice
+//! dice".
+
+use std::path::Path;
+
+use hammer_core::{ArtifactHash, Store};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("io: {0}")]
+    Io(#[from] std::io::Error),
+    #[error("core: {0}")]
+    Core(#[from] hammer_core::Error),
+    #[error(
+        "integridad: el artefacto '{dir}' recibido hashea a {got} pero el índice anuncia {want} — \
+         transferencia corrupta o manipulada; NO se selló"
+    )]
+    Verify { dir: String, want: String, got: String },
+    #[error("mirror: {0}")]
+    Other(String),
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// Una entrada del índice del store: el directorio del artefacto (`<hash>-<name>`) y su **content-hash**
+/// (`of_tree`, `b3:…`). El `dir` es la dirección (input-addressed) y `content` la huella de los bytes:
+/// dos artefactos con el mismo `dir` deben tener el mismo `content` (coherencia del CAS).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ArtifactRef {
+    pub dir: String,
+    pub content: String,
+}
+
+/// Índice de un store: la lista de sus artefactos con su content-hash, ordenada por `dir` (estable).
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct StoreIndex {
+    pub artifacts: Vec<ArtifactRef>,
+}
+
+impl StoreIndex {
+    /// Busca un artefacto por su `dir`.
+    pub fn get(&self, dir: &str) -> Option<&ArtifactRef> {
+        self.artifacts.iter().find(|a| a.dir == dir)
+    }
+}
+
+/// ¿El nombre de directorio tiene forma de artefacto del store? = 64 hex + `-` + `<name>` no vacío. Así
+/// se ignoran `bootstrap.json`, `.bootstrap-tmp`, índices y otros ficheros del root del store.
+fn is_artifact_dir(name: &str) -> bool {
+    let bytes = name.as_bytes();
+    bytes.len() > 65
+        && bytes[64] == b'-'
+        && bytes[..64].iter().all(|b| b.is_ascii_hexdigit())
+}
+
+/// Parte `<64hex>-<name>` en `(ArtifactHash, name)`. El `name` puede contener `-` (p.ej.
+/// `product-attested-rootfs`); sólo el primer `-`, en la posición 64, separa el hash.
+fn parse_dir(dir: &str) -> Option<(ArtifactHash, String)> {
+    if !is_artifact_dir(dir) {
+        return None;
+    }
+    Some((ArtifactHash::from_hex(&dir[..64]), dir[65..].to_string()))
+}
+
+/// Construye el índice de un store: para cada directorio-artefacto, su `of_tree`. Un store inexistente
+/// se trata como vacío (el destino aún no creado en un primer `pull`). Determinista (ordenado por `dir`).
+pub fn index(store_root: &Path) -> Result<StoreIndex> {
+    let mut artifacts = Vec::new();
+    if store_root.is_dir() {
+        for entry in std::fs::read_dir(store_root)? {
+            let entry = entry?;
+            let name = entry.file_name().to_string_lossy().into_owned();
+            if !is_artifact_dir(&name) || !entry.path().is_dir() {
+                continue;
+            }
+            let content = ArtifactHash::of_tree(&entry.path())?.as_str().to_string();
+            artifacts.push(ArtifactRef { dir: name, content });
+        }
+    }
+    artifacts.sort_by(|a, b| a.dir.cmp(&b.dir));
+    Ok(StoreIndex { artifacts })
+}
+
+/// Diferencia entre dos stores: `(faltan_en_dst, faltan_en_src, conflictos)`. Un **conflicto** es un
+/// artefacto presente en ambos con el MISMO `dir` pero DISTINTO `content` (misma dirección, otros bytes:
+/// rotura de la coherencia del CAS — no-reproducibilidad o corrupción). No copia nada.
+pub fn diff(
+    src_root: &Path,
+    dst_root: &Path,
+) -> Result<(Vec<ArtifactRef>, Vec<ArtifactRef>, Vec<String>)> {
+    let si = index(src_root)?;
+    let di = index(dst_root)?;
+    let mut missing_in_dst = Vec::new();
+    let mut conflicts = Vec::new();
+    for a in &si.artifacts {
+        match di.get(&a.dir) {
+            None => missing_in_dst.push(a.clone()),
+            Some(d) if d.content != a.content => conflicts.push(a.dir.clone()),
+            Some(_) => {}
+        }
+    }
+    let missing_in_src = di
+        .artifacts
+        .iter()
+        .filter(|d| si.get(&d.dir).is_none())
+        .cloned()
+        .collect();
+    Ok((missing_in_dst, missing_in_src, conflicts))
+}
+
+/// Reporte de un [`sync`]: qué se copió, cuántos ya estaban, conflictos y fallos de verificación.
+#[derive(Debug, Clone, Default)]
+pub struct SyncReport {
+    /// `dir`s copiados+verificados+sellados en el destino.
+    pub copied: Vec<String>,
+    /// Ya presentes con el mismo content-hash (idempotencia).
+    pub skipped_present: usize,
+    /// Presentes en ambos con content-hash distinto (CAS incoherente; NO se sobrescriben).
+    pub conflicts: Vec<String>,
+    /// `(dir, motivo)` de los que fallaron la verificación de integridad (no se sellaron).
+    pub failed: Vec<(String, String)>,
+}
+
+impl SyncReport {
+    /// Éxito = nada falló la verificación y no hubo conflictos.
+    pub fn ok(&self) -> bool {
+        self.failed.is_empty() && self.conflicts.is_empty()
+    }
+}
+
+/// Copia el árbol de un artefacto a un staging bajo el destino, **recomputa `of_tree`**, exige que case
+/// `expected_content` y sólo entonces lo **sella** en `dst_store` (rename atómico + read-only, vía
+/// [`Store::seal`]). El staging vive bajo el root del destino ⇒ mismo filesystem ⇒ el rename del seal es
+/// atómico; ante un hash que no casa, se borra el staging y NO se instala nada.
+fn install_verified(
+    src_dir: &Path,
+    dst_store: &Store,
+    h: &ArtifactHash,
+    name: &str,
+    expected_content: &str,
+) -> Result<()> {
+    let dir_label = format!("{}-{name}", h.as_str().trim_start_matches("b3:"));
+    let staging = dst_store
+        .root()
+        .join(".mirror-tmp")
+        .join(format!("{dir_label}.incoming"));
+    let _ = std::fs::remove_dir_all(&staging);
+    if let Some(parent) = staging.parent() {
+        std::fs::create_dir_all(parent)?;
+    }
+
+    let result = (|| -> Result<()> {
+        copy_tree(src_dir, &staging)?;
+        let got = ArtifactHash::of_tree(&staging)?;
+        if got.as_str() != expected_content {
+            return Err(Error::Verify {
+                dir: dir_label.clone(),
+                want: expected_content.to_string(),
+                got: got.as_str().to_string(),
+            });
+        }
+        dst_store.seal(&staging, h, name)?;
+        Ok(())
+    })();
+    let _ = std::fs::remove_dir_all(&staging);
+    result
+}
+
+/// **Sincroniza** `src`→`dst`: copia a `dst` cada artefacto que le falta (verificando `of_tree` antes de
+/// sellar), salta los ya presentes con el mismo contenido y reporta conflictos (mismo `dir`, otro
+/// contenido). Idempotente: re-correr sobre un destino ya al día copia cero. Es el motor de `push`
+/// (`sync(local, remoto)`) y `pull` (`sync(remoto, local)`).
+pub fn sync(src_root: &Path, dst_root: &Path) -> Result<SyncReport> {
+    let si = index(src_root)?;
+    let di = index(dst_root)?;
+    let dst_store = Store::open(dst_root)?;
+    let mut report = SyncReport::default();
+
+    for a in &si.artifacts {
+        match di.get(&a.dir) {
+            Some(d) if d.content == a.content => {
+                report.skipped_present += 1;
+                continue;
+            }
+            Some(_) => {
+                report.conflicts.push(a.dir.clone());
+                continue;
+            }
+            None => {}
+        }
+        let (h, name) = match parse_dir(&a.dir) {
+            Some(p) => p,
+            None => continue, // index() ya filtra, defensivo
+        };
+        match install_verified(&src_root.join(&a.dir), &dst_store, &h, &name, &a.content) {
+            Ok(()) => {
+                tracing::info!(dir = %a.dir, "mirror: artefacto replicado y verificado");
+                report.copied.push(a.dir.clone());
+            }
+            Err(e) => {
+                tracing::warn!(dir = %a.dir, error = %e, "mirror: fallo de replicación");
+                report.failed.push((a.dir.clone(), e.to_string()));
+            }
+        }
+    }
+    Ok(report)
+}
+
+/// Copia recursiva `src`→`dst` preservando symlinks (target literal) y modos (incl. bit de ejecución,
+/// que `of_tree` mira). El destino se asume inexistente (staging recién limpiado).
+fn copy_tree(src: &Path, dst: &Path) -> Result<()> {
+    let meta = std::fs::symlink_metadata(src)?;
+    let ft = meta.file_type();
+    if ft.is_symlink() {
+        let target = std::fs::read_link(src)?;
+        std::os::unix::fs::symlink(target, dst)?;
+    } else if ft.is_dir() {
+        std::fs::create_dir_all(dst)?;
+        for entry in std::fs::read_dir(src)? {
+            let entry = entry?;
+            copy_tree(&entry.path(), &dst.join(entry.file_name()))?;
+        }
+    } else {
+        std::fs::copy(src, dst)?;
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Sella un artefacto sintético `<hex>-<name>` en `root` con los ficheros que pinte `build`.
+    fn seal(root: &Path, hex: &str, name: &str, build: impl Fn(&Path)) -> String {
+        let store = Store::open(root).unwrap();
+        let h = ArtifactHash::from_hex(hex);
+        let staging = root.join(".bootstrap-tmp").join(h.store_dir_name(name));
+        let _ = std::fs::remove_dir_all(&staging);
+        std::fs::create_dir_all(&staging).unwrap();
+        build(&staging);
+        store.seal(&staging, &h, name).unwrap();
+        h.store_dir_name(name)
+    }
+
+    fn h64(prefix: &str) -> String {
+        format!("{prefix}{}", "0".repeat(64 - prefix.len()))
+    }
+
+    #[test]
+    fn artifact_dir_shape_filters_non_artifacts() {
+        assert!(is_artifact_dir(&format!("{}-busybox", h64("ab"))));
+        assert!(is_artifact_dir(&format!("{}-product-attested-rootfs", h64("ff"))));
+        assert!(!is_artifact_dir("bootstrap.json"));
+        assert!(!is_artifact_dir(".bootstrap-tmp"));
+        assert!(!is_artifact_dir(&format!("{}-", h64("ab"))), "sin name");
+        assert!(!is_artifact_dir(&format!("{}xy-z", h64("ab"))), "65 chars no-hex antes del -");
+        // parse_dir respeta los '-' del name.
+        let (hh, name) = parse_dir(&format!("{}-find-utils-xargs", h64("cd"))).unwrap();
+        assert_eq!(name, "find-utils-xargs");
+        assert!(hh.as_str().starts_with("b3:"));
+    }
+
+    #[test]
+    fn index_lists_artifacts_with_content_hash_and_skips_junk() {
+        let tmp = tempfile::tempdir().unwrap();
+        let root = tmp.path();
+        seal(root, &h64("aa"), "busybox", |w| {
+            std::fs::write(w.join("bin"), b"\x7fELF").unwrap();
+        });
+        // ruido del root del store que NO debe entrar al índice.
+        std::fs::write(root.join("bootstrap.json"), b"{}").unwrap();
+        std::fs::create_dir_all(root.join(".bootstrap-tmp")).unwrap();
+
+        let idx = index(root).unwrap();
+        assert_eq!(idx.artifacts.len(), 1, "sólo el artefacto, no bootstrap.json ni .bootstrap-tmp");
+        assert!(idx.artifacts[0].dir.ends_with("-busybox"));
+        assert!(idx.artifacts[0].content.starts_with("b3:"));
+        // content == of_tree real del dir.
+        let ot = ArtifactHash::of_tree(&root.join(&idx.artifacts[0].dir)).unwrap();
+        assert_eq!(idx.artifacts[0].content, ot.as_str());
+    }
+
+    #[test]
+    fn sync_replicates_missing_and_is_idempotent() {
+        let src = tempfile::tempdir().unwrap();
+        let dst = tempfile::tempdir().unwrap();
+        seal(src.path(), &h64("aa"), "busybox", |w| {
+            std::fs::write(w.join("bin"), b"\x7fELF-bb").unwrap();
+            std::os::unix::fs::symlink("bin", w.join("sh")).unwrap();
+        });
+        seal(src.path(), &h64("bb"), "uutils", |w| {
+            std::fs::create_dir_all(w.join("usr/bin")).unwrap();
+            std::fs::write(w.join("usr/bin/coreutils"), b"\x7fELF-uu").unwrap();
+        });
+
+        let r = sync(src.path(), dst.path()).unwrap();
+        assert!(r.ok());
+        assert_eq!(r.copied.len(), 2);
+        // el destino quedó con los mismos artefactos y mismos content-hash.
+        let (md, ms, conf) = diff(src.path(), dst.path()).unwrap();
+        assert!(md.is_empty() && conf.is_empty(), "destino al día");
+        assert!(ms.is_empty(), "el destino no tiene de más");
+        // los bytes (y el symlink) viajaron.
+        let bbdir = dst.path().join(format!("{}-busybox", h64("aa")));
+        assert_eq!(std::fs::read(bbdir.join("bin")).unwrap(), b"\x7fELF-bb");
+        assert_eq!(std::fs::read_link(bbdir.join("sh")).unwrap(), std::path::PathBuf::from("bin"));
+
+        // 2ª corrida: idempotente, copia 0.
+        let r2 = sync(src.path(), dst.path()).unwrap();
+        assert!(r2.copied.is_empty() && r2.skipped_present == 2);
+        assert!(!dst.path().join(".mirror-tmp").join(format!("{}-busybox.incoming", h64("aa"))).exists(),
+            "el staging se limpió");
+    }
+
+    #[test]
+    fn sync_rejects_corrupted_transfer_without_sealing() {
+        // El guardrail de integridad: si lo que llega NO hashea a lo que el índice anuncia, se rechaza.
+        let dst = tempfile::tempdir().unwrap();
+        let src_dir = tempfile::tempdir().unwrap();
+        std::fs::write(src_dir.path().join("bin"), b"bytes-reales").unwrap();
+        let store = Store::open(dst.path()).unwrap();
+        let h = ArtifactHash::from_hex(&h64("cc"));
+        // expected_content MENTIROSO (no es el of_tree real del src) ⇒ debe fallar Verify y no sellar.
+        let err = install_verified(src_dir.path(), &store, &h, "evil", "b3:deadbeef").unwrap_err();
+        assert!(matches!(err, Error::Verify { .. }), "esperaba Verify, vino {err}");
+        assert!(!store.has(&h, "evil"), "un artefacto que no verifica NO se sella");
+        assert!(!dst.path().join(".mirror-tmp").join(format!("{}-evil.incoming", h64("cc"))).exists(),
+            "staging limpiado tras el rechazo");
+    }
+
+    #[test]
+    fn sync_flags_conflicts_and_does_not_overwrite() {
+        // Mismo dir en src y dst pero contenido distinto = conflicto (CAS incoherente). No se pisa.
+        let src = tempfile::tempdir().unwrap();
+        let dst = tempfile::tempdir().unwrap();
+        seal(src.path(), &h64("dd"), "x", |w| std::fs::write(w.join("f"), b"SRC").unwrap());
+        seal(dst.path(), &h64("dd"), "x", |w| std::fs::write(w.join("f"), b"DST-distinto").unwrap());
+
+        let r = sync(src.path(), dst.path()).unwrap();
+        assert!(!r.ok(), "hay conflicto ⇒ no-ok");
+        assert_eq!(r.conflicts.len(), 1);
+        assert!(r.copied.is_empty());
+        // el destino conserva SUS bytes (no se sobrescribió).
+        assert_eq!(std::fs::read(dst.path().join(format!("{}-x", h64("dd"))).join("f")).unwrap(), b"DST-distinto");
+    }
+}
diff --git a/docs/13-release-engineering.md b/docs/13-release-engineering.md
index 1f3e44fa..14fd397d 100644
--- a/docs/13-release-engineering.md
+++ b/docs/13-release-engineering.md
@@ -52,8 +52,17 @@ kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-m
   un *product rootfs* lean es refinamiento.
 - **E2 — instalador a disco físico:** un `hammer install <device>` que particiona un disco real y
   vuelca las tres particiones + GRUB (la misma lógica, apuntando a `/dev/sdX` en vez de un fichero).
-- **E3 — mirror del store:** servir/replicar `/store` content-addressed (BLAKE3) entre máquinas; el
-  hash ES la dirección, así que un mirror es un CAS replicado + resolución por hash.
+- **E3 — mirror del store: ✅ primer corte** (crate `hammer-mirror` + CLI `hammer mirror push|pull|status`).
+  Replica `/store` content-addressed (BLAKE3) entre máquinas; el hash ES la dirección, así que un mirror es
+  un CAS replicado + resolución por hash. La integridad se ancla en `of_tree` (content-hash del árbol): el
+  receptor **recomputa** `of_tree` sobre lo recibido y exige que case el del índice **antes** de sellar
+  (`Store::seal` atómico + read-only) — una transferencia corrupta/manipulada se **rechaza**, no se instala.
+  `push`/`pull` son `sync(src,dst)` (idempotente, salta presentes, reporta conflictos = mismo hash con otro
+  contenido). Transporte de esta entrega = **sistema de ficheros** (el remoto es una ruta a otro store, p.ej.
+  un montaje sshfs); el transporte sobre SSH/red que el producto ya expone es un envoltorio posterior.
+  Endurecimiento pendiente: anclar el `of_tree` a una raíz firmada (log de transparencia `bootstrap.json` /
+  atestación de Etapa D) para defenderse de un origen plenamente malicioso (hoy el invariante forzado es
+  "el contenido recibido hashea a lo que el índice anuncia", que ataja corrupción de transporte).
 - **E4 — upgrades:** aplicar un nuevo árbol Stage 1 sin reinstalar — atómico vía el modelo overlay +
   el journal (D), con rollback al árbol anterior.
 - **E5 — ISO/medio de arranque:** medio live para correr el instalador (xorriso/grub-mkrescue;