H1b (proof-carrying recipes): checker swm-verify --evidence que ejecuta la evidencia

El verificador que corre cada check en el sandbox reproducible y falla ⇒ no proponer
(SDD 15 §H1). Piezas: (1) hammer-core: EvidenceCheck::evaluate(exit, stdout) -> CheckOutcome
(pura, testeada: exige expected_exit y, si hay, blake3(stdout)==expected_output) +
ArtifactHash::of_bytes. (2) hammer-build: Sandbox::run_capture -> CmdOutcome (captura stdout
completo + exit, tee de stderr) + run_evidence(recipe,cfg,store) -> EvidenceReport que
reproduce el artefacto (cache-hit), levanta un sandbox con fuente+build-deps+el artefacto
instalado como capa overlay (binarios en PATH) y corre cada check. (3) CLI: swm-verify
--evidence reconstruye cada source_patch y corre run_evidence; imprime veredicto por check +
estrato máximo alcanzado; exit != 0 si algún check falla. Es un runner de comandos con hash
del output, no un framework — la confianza vive en el checker.

Verificado e2e: tree con [[evidence.checks]] cmd-exit 'tree --version' → pack cache-hitea
(evidencia no cambia el hash) → swm-verify --evidence corre el check en el sandbox: pasa (exit
0) y falla con expected_exit=7 (exit 1, 'NO proponer'). Núcleo puro con tests unitarios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 13:03:34 -04:00
co-authored by Claude Opus 4.8
parent 39f0f26e7b
commit 497d0d00e7
6 changed files with 316 additions and 5 deletions
+83 -2
View File
@@ -134,6 +134,10 @@ enum Cmd {
/// /var/lib/hammer/trust. La firma reporta estado, NO autoriza promover (ver SDD 09).
#[arg(long)]
trust: Option<PathBuf>,
/// [H1] Además del schema/firma, REPRODUCE cada paquete source_patch y corre su bloque
/// `evidence` (proof-carrying recipes) en el sandbox. Falla ⇒ exit != 0 (no proponer).
#[arg(long)]
evidence: bool,
},
/// [Fase 4] Genera un par de claves Ed25519 para firmar `.swm`. Escribe `<name>.ed25519`
/// (privada, 0600) y `<name>.ed25519.pub` en `--out` (default: /var/lib/hammer/trust).
@@ -798,8 +802,14 @@ fn main() -> anyhow::Result<()> {
None, // `apply` aplica un .swm genérico: no conoce el nombre del paquete
)?;
}
Cmd::SwmVerify { file, base_ref, trust } => {
run_swm_verify(&file, base_ref.as_deref(), trust.as_deref())?;
Cmd::SwmVerify { file, base_ref, trust, evidence } => {
run_swm_verify(
&file,
base_ref.as_deref(),
trust.as_deref(),
evidence,
std::path::Path::new(&cli.store),
)?;
}
Cmd::Keygen { name, out } => {
run_keygen(&name, out.as_deref())?;
@@ -1208,6 +1218,8 @@ fn run_swm_verify(
file: &str,
base_ref: Option<&std::path::Path>,
trust: Option<&std::path::Path>,
evidence: bool,
store_path: &std::path::Path,
) -> anyhow::Result<()> {
let swm = load_swm(file)?;
swm.verify_schema()
@@ -1228,9 +1240,78 @@ fn run_swm_verify(
eprintln!("verify_base: omitido (sin --base-ref)");
}
print_sig_status(&swm, trust)?;
if evidence {
run_swm_evidence(&swm, store_path)?;
}
Ok(())
}
/// [H1b] Reproduce cada paquete `source_patch` del `.swm` y corre su bloque `evidence` en el sandbox
/// reproducible. Imprime el veredicto por check y ABORTA (exit != 0) si alguno falla — el gate de
/// "reproduce + estas N pruebas pasan" antes de que el humano proponga (SDD 15 §H1). Frontera:
/// paquetes con build-deps necesitan esas recetas resolubles en el catálogo del lab (como `install`);
/// para el caso self-contained (binario estático sin deps) alcanza con el `.swm`.
fn run_swm_evidence(swm: &hammer_core::Swm, store_path: &std::path::Path) -> anyhow::Result<()> {
// Corto-circuito ANTES de tocar el store/catálogo: si ninguna mutación trae evidencia, no hay
// nada que reproducir (y no forzamos abrir un store que quizá no exista/no sea escribible).
let has_evidence = swm.mutations.iter().any(|m| {
matches!(m, hammer_core::Mutation::SourcePatch { evidence, .. } if !evidence.is_empty())
});
if !has_evidence {
println!("evidence: ninguna mutación declara evidencia (nada que verificar)");
return Ok(());
}
let store = hammer_core::Store::open(store_path)?;
let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
let catalog_dir = hammer_build::swm_bridge::catalog_dir_for(&cfg, None);
std::fs::create_dir_all(&catalog_dir)?;
let mut ran = 0usize;
let mut failed = 0usize;
for m in &swm.mutations {
let hammer_core::Mutation::SourcePatch { evidence, target_bin, .. } = m else {
continue;
};
if evidence.is_empty() {
continue;
}
let recipe =
hammer_build::swm_bridge::recipe_from_source_patch(m, None, &catalog_dir)?;
println!(
"evidence: reproduciendo {target_bin} y corriendo {} check(s)…",
evidence.checks.len()
);
let report = hammer_build::run_evidence(&recipe, &cfg, &store)?;
for r in &report.results {
let mark = if r.outcome.passed { "" } else { "" };
println!(
" {mark} [{}] {}{}",
r.kind.as_str(),
r.cmd,
r.outcome.detail
);
}
ran += report.results.len();
failed += report.results.iter().filter(|r| !r.outcome.passed).count();
if report.all_passed() {
if let Some(lvl) = report.max_level_passed() {
println!(" → verificado hasta el estrato '{}'", lvl.as_str());
}
}
}
if ran == 0 {
println!("evidence: ninguna mutación declara evidencia (nada que verificar)");
Ok(())
} else if failed > 0 {
anyhow::bail!("evidence: {failed}/{ran} check(s) fallaron ⇒ NO proponer");
} else {
println!("evidence: {ran} check(s), todos pasan ✓");
Ok(())
}
}
const DEFAULT_TRUST_DIR: &str = "/var/lib/hammer/trust";
/// Carga el TrustStore (de `--trust` o el default) y reporta el estado de la firma. La firma