# 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](09-trust-model.md)). ## 1. Esquema ```yaml # 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 `.swm`s autocontenidos (tests, snapshots offline, datos pequeños). - `content_url`: el receptor lo descarga. En ambos casos se verifica `content_hash` (BLAKE3) antes de escribir nada. Cada `source_patch` es, en esencia, una `Recipe` ([SDD 02](02-build-lab.md)) serializada para viajar. El `build` lleva el **compilador por mutación** (no global). ## 3. Aplicación (`hammer apply`) ``` hammer apply my-grep-tweak.swm ``` 1. **Verifica la base.** ¿Los `pins`/`distro_version` son compatibles con el sistema local? Si no, avisa qué difiere y se detiene (o `--force` bajo tu responsabilidad). 2. **Verifica la firma** (si existe) contra las claves de confianza locales. 3. **Reproduce.** Para cada `source_patch`: clona el repo al commit, aplica el patch, compila en el lab local → obtiene un `artifact_hash`. 4. **Aísla.** Hidrata todo en un **overlay temporal** ([SDD 04](04-overlay.md)); aplica `config_edit`/`init_rule`/`file_drop` en esa capa. **No toca el sistema base.** 5. **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](02-build-lab.md)), 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_drop` para 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: `target` viaja en cada `source_patch`. ## 6. Interfaz ```rust // hammer-core #[derive(Serialize, Deserialize)] pub struct Swm { /* swm_version, base, mutations, signature */ } impl Swm { pub fn from_yaml(s: &str) -> Result; pub fn to_yaml(&self) -> Result; pub fn verify_base(&self, local: &BaseRef) -> BaseCompat; pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus; } ``` CLI: - `hammer apply ` — abre un overlay por defecto y aplica las mutaciones. Con `--prefix DIR` opera directamente bajo `DIR` sin overlay (tests, staging). Con `--base-ref base.json` aborta si la base local no es compatible. - `hammer swm-verify ` — chequea schema, hashes de los `file_drop` inline y, con `--base-ref`, la compatibilidad de base. - `hammer export --journal DIR > out.swm` — lee el diario y emite un `.swm` con un `file_drop` por archivo modificado (estado final actual; los `Delete` se omiten). El receptor reproduce byte-a-byte. (Cuando el diario tiene provenance de receta, `export` emite `source_patch` en vez de `file_drop` — el mapa artefacto→receta ya existe.) - `hammer pack ` — **dirección forward (Etapa F):** empaqueta una receta del corpus como un `.swm` de un único `source_patch`. Vuelve una receta que el sistema ya sabe construir un paquete distribuible y reproducible-desde-fuente; es la inversa de `apply`. `--target-bin` fija el ancla de sanity (default `/usr/bin/`); `--expected b3:…` o `--build` sellan el `expected_hash` (ancla "verificar, no confiar"); `--sign KEY` lo firma. El `source_patch` traslada con fidelidad `build.{phases,zig_version,flags}`, en tarball `strip_components`, y las `deps` por nombre; los patches de la receta viajan inline (concatenados). Si la receta declara build-deps, `pack` recuerda publicarlas en el mismo repo. Con `--repo DIR` **publica** en un repositorio (ver abajo) en vez de (o además de) `--out`. - `hammer install [--repo DIR|URL]` — **consume** del repositorio: resuelve `nombre` en el índice, verifica la firma del release y la del `.swm` (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 de `apply` (reproduce el `source_patch` desde fuente, con sus deps materializadas en el sandbox, + hidrata). Nunca corre 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. `--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 (`/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). - `hammer repo verify --repo DIR [--trust DIR]` — verifica la firma del release. - `hammer uninstall [--db FILE]` — borra los ficheros que el paquete registró (refcount: respeta los que otro paquete instalado también aporta) y lo quita de la DB de instalados. - `hammer installed [--db FILE]` — lista los paquetes instalados. `install` registra cada paquete (nombre, versión, hash, ficheros creados) en la DB (`/var/lib/hammer/installed.json` por defecto). - `hammer import-nix [FILE]` — **Etapa G (poblar el catálogo desde nixpkgs):** toma el JSON normalizado de un paquete nix (lo produce `scripts/nix-import.sh ` vía `nix eval`) y emite una receta hammer. Trae la RECETA (source+hash+deps+flags), NUNCA el binario del cache de nix — hammer reconstruye desde fuente igual ("verificar, no confiar"). `fetchurl` plano → `tarball` +sha256 (hash nix→hex); GitHub → `repo`+`commit`. Es un PUNTO DE PARTIDA: el build en el lab de hammer (zig/musl) suele necesitar adaptación por paquete. Pipeline: `nix-import.sh hello` → receta → `pack` → `.swm` → repo → `install` (reproduce desde fuente). Es cómo el catálogo crece de 34 recetas a-mano a miles sin reescribirlas. ## 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 repo son ficheros estáticos, así que `install --repo URL` lo consume por HTTP(S) (cualquier servidor estático sirve; baja `index.json` + el cierre de deps a un temporal). 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 ` 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. **Firma del release.** Además de la firma de cada `.swm` (autoría del paquete), el ÍNDICE entero se puede firmar como release (`RepoIndex::signature`, Ed25519 sobre la lista de paquetes canónica). Ancla qué paquetes/versiones/hashes existen: un atacante no puede añadir, quitar ni intercambiar entradas sin invalidar la firma. Cualquier `upsert` (publicar) la invalida ⇒ re-firmar tras publicar. `install` verifica el release antes de resolver: una firma mala aborta ("el índice fue manipulado"). Mismo primitivo Ed25519 que el `.swm` (`sign::{sign_raw,verify_raw}`).