Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.
Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.
- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
+ base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.6 KiB
SDD 06 — Formato .swm (Software Mutación)
El .swm es la unidad de intercambio de hammer. No es un paquete binario (.deb,
.rpm, ni un PKGBUILD que compila quién-sabe-qué). Es un manifiesto de la mutación: la
receta de transformación sobre código fuente público, más las ediciones de configuración. El
receptor reproduce y verifica localmente; nunca ejecuta tu binario (ver
SDD 09).
1. Esquema
# my-grep-tweak.swm
swm_version: 1
base:
distro_version: "2026-06-06" # versión del set base de hammer
pins: # commits fijados de los upstreams relevantes
kernel: "a1b2c3d…"
musl: "e4f5a6b…"
# base_hash deriva de pins+distro_version; el receptor verifica compatibilidad
mutations:
- type: source_patch # recompilar una herramienta desde su fuente parcheada
repo: "git://git.savannah.gnu.org/grep.git"
commit: "a1b2c3d…" # commit fijado sobre el que aplica el patch
patch: | # parche .patch de git, embebido o por URL (patch_url)
--- a/src/main.c
+++ b/src/main.c
@@ ...
build:
compiler: "zig-cc"
target: "x86_64-linux-musl"
link: "static"
flags: ["--enable-perl-regexp"]
target_bin: "/bin/grep"
- type: config_edit # editar un archivo de config en texto plano
file: "/etc/network.conf"
inline_diff: |
- DHCP=yes
+ IP=192.168.1.100
- type: init_rule # añadir/ajustar una regla del init (ver SDD 07)
action: "watchdog"
service: "web"
command: "/bin/myweb -p 8080"
signature: # opcional pero recomendado (ver SDD 09)
by: "sergio"
alg: "ed25519"
sig: "base64…"
2. Tipos de mutación
type |
Qué describe | Cómo lo aplica el receptor |
|---|---|---|
source_patch |
recompilar una herramienta desde fuente parcheada (git o tarball) | clona repo@commit / baja tarball+sha256 → aplica patch → hammer build → hidrata en overlay |
config_edit |
edición de un archivo de config | aplica el inline_diff (3-way) sobre el archivo objetivo |
init_rule |
regla de supervisión de un servicio | materializa /etc/hammer/init.d/{service}.rule (TOML: service/action/command); disable/stop la retiran. El init (arje) lee ese árbol |
file_drop |
depositar un archivo de datos no compilable | escribe el archivo (con su hash declarado) en la ruta |
file_drop admite dos modos para el contenido, mutuamente excluyentes:
content_b64(RFC 4648, sin saltos): inline en el.swm. Útil para.swms autocontenidos (tests, snapshots offline, datos pequeños).content_url: el receptor lo descarga. En ambos casos se verificacontent_hash(BLAKE3) antes de escribir nada.
Cada source_patch es, en esencia, una Recipe (SDD 02) serializada para
viajar. El build lleva el compilador por mutación (no global).
3. Aplicación (hammer apply)
hammer apply my-grep-tweak.swm
- Verifica la base. ¿Los
pins/distro_versionson compatibles con el sistema local? Si no, avisa qué difiere y se detiene (o--forcebajo tu responsabilidad). - Verifica la firma (si existe) contra las claves de confianza locales.
- Reproduce. Para cada
source_patch: clona el repo al commit, aplica el patch, compila en el lab local → obtiene unartifact_hash. - Aísla. Hidrata todo en un overlay temporal (SDD 04); aplica
config_edit/init_rule/file_dropen esa capa. No toca el sistema base. - Valida. Corres/pruebas en caliente. Si bien →
hammer commit. Si mal →hammer discard.
Nunca se ejecuta un binario ajeno: sólo se transforma código fuente público con una receta que tu propio laboratorio compila.
4. Determinismo y verificación cruzada
Como el lab es determinista (SDD 02), el artifact_hash que produce tu
máquina debe coincidir con el del autor (si declara expected_hash). Coincidencia ⇒ obtuviste
exactamente el mismo binario, sin haberlo descargado. Discrepancia ⇒ algo difiere (toolchain,
pin, patch) y hammer te dice qué. Esto es "verificar, no confiar" en la práctica.
5. No-objetivos del formato
- No transporta binarios cocidos (salvo
file_droppara datos no compilables, con hash). - No es Turing-completo: es declarativo para el intercambio, aunque el sistema que describe se viva imperativamente. (Lo declarativo aquí es honesto: describe una transformación, no aprisiona tu sistema.)
- No asume una arquitectura:
targetviaja en cadasource_patch.
6. Interfaz
// hammer-core
#[derive(Serialize, Deserialize)]
pub struct Swm { /* swm_version, base, mutations, signature */ }
impl Swm {
pub fn from_yaml(s: &str) -> Result<Swm>;
pub fn to_yaml(&self) -> Result<String>;
pub fn verify_base(&self, local: &BaseRef) -> BaseCompat;
pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus;
}
CLI:
hammer apply <file.swm>— abre un overlay por defecto y aplica las mutaciones. Con--prefix DIRopera directamente bajoDIRsin overlay (tests, staging). Con--base-ref base.jsonaborta si la base local no es compatible.hammer swm-verify <file.swm>— chequea schema, hashes de losfile_dropinline y, con--base-ref, la compatibilidad de base.hammer export --journal DIR > out.swm— lee el diario y emite un.swmcon unfile_droppor archivo modificado (estado final actual; losDeletese omiten). El receptor reproduce byte-a-byte. (Cuando el diario tiene provenance de receta,exportemitesource_patchen vez defile_drop— el mapa artefacto→receta ya existe.)hammer pack <recipe.toml>— dirección forward (Etapa F): empaqueta una receta del corpus como un.swmde un únicosource_patch. Vuelve una receta que el sistema ya sabe construir un paquete distribuible y reproducible-desde-fuente; es la inversa deapply.--target-binfija el ancla de sanity (default/usr/bin/<name>);--expected b3:…o--buildsellan elexpected_hash(ancla "verificar, no confiar");--sign KEYlo firma. Elsource_patchtraslada con fidelidadbuild.{phases,zig_version,flags}, en tarballstrip_components, y lasdepspor nombre; los patches de la receta viajan inline (concatenados). Si la receta declara build-deps,packrecuerda publicarlas en el mismo repo. Con--repo DIRpublica en un repositorio (ver abajo) en vez de (o además de)--out.hammer install <nombre> [--repo DIR]— consume del repositorio: resuelvenombreen el índice, verifica la firma (con--trust DIR) y la base (con--base-ref), resuelve el cierre transitivo de build-deps desde el índice (orden topológico) poblando un catálogo de recetas, y delega en el camino deapply(reproduce elsource_patchdesde fuente, con sus deps materializadas en el sandbox, + hidrata). Nunca corre un binario ajeno.--prefix/--skip-source-patchpara staging y dry-run de schema.hammer repo list [--repo DIR]— lista el catálogo (<repo>/index.json).
7. Repositorio de paquetes (Etapa F)
Un repo es un directorio con los .swm + un index.json que mapea nombre → paquete
({name, version, file, distro_version, expected_hash?, signed_by?}, orden alfabético,
diffeable). Un .swm no lleva identidad propia (es un manifiesto de mutación, no "el paquete
X"); la identidad la asigna el repo al publicar — el índice es el namespace. pack --repo
publica con el nombre/versión de la receta (upsert idempotente por nombre; una versión nueva
retira el .swm huérfano). El transporte del repo (filesystem / sshfs / mirror) es ortogonal:
ver hammer-mirror para el CAS del store. Tipos en hammer-core::repo (RepoIndex,
PackageEntry).
Dependencias. Un source_patch lleva sus build-deps por NOMBRE (las de la receta original);
la PackageEntry las espeja para resolver el grafo sin abrir cada .swm. install <nombre>
calcula el cierre transitivo (RepoIndex::resolve_closure, topológico, error si falta una dep o
hay ciclo) y, para reproducir, reconstruye un catálogo de recetas efímero: escribe un
{dep}.toml por dep (sintetizado de su .swm vía swm_bridge::recipe_from_source_patch) en el
catalog_dir determinista que el lab consulta al materializar build-deps en el sandbox. Así un
paquete con deps (p. ej. bwrap→libcap, openssh→zlib,openssl) se reproduce desde fuente
bit-idéntico — verificado: install bwrap reproduce el artefacto cacheado exacto resolviendo
libcap del catálogo.