Files
hammer/docs/06-swm-format.md
T
sergioandClaude Opus 4.8 b979d9e550 Etapa G: importador nix→receta (hammer import-nix + scripts/nix-import.sh)
Poblar el catálogo no es opcional: 34 recetas a mano = userland desierto. nixpkgs es el mayor set
de recetas DESDE FUENTE ⇒ semilla natural. Importamos la RECETA (source+hash+deps), nunca el
binario del cache de nix — hammer reconstruye desde fuente ("verificar, no confiar").

- crates/hammer-cli/nix_import.rs: consume el JSON normalizado de nix y emite una receta hammer.
  Clasifica el origen: fetchurl flat → tarball+sha256 (convierte el hash nix SRI/base32/hex → hex);
  fetchFromGitHub → repo+commit (hammer pinea por commit, no necesita el hash NAR). Filtra el ruido
  de stdenv (setup-hooks, wrappers). nix_base32 decode portado. 11 tests.
- `hammer import-nix [FILE|-]` (stdin) → receta .toml; valida que parsee como Recipe.
- scripts/nix-import.sh <attr>: `nix eval --apply` produce el JSON normalizado y lo pipea al
  importador. NIX_STORE= para store local si /nix/store no es escribible.
- VALIDADO contra nixpkgs REAL (nix 2.34): import hello (tarball, sha256→hex) + ripgrep (github→
  repo+commit); pipeline completo nix→import→pack→.swm probado con hello. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:41:50 -04:00

11 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 verifica content_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
  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); 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), 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

// 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 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 <file.swm> — 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 <recipe.toml>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/<name>); --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 <nombre> [--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 (<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).
  • hammer repo verify --repo DIR [--trust DIR] — verifica la firma del release.
  • hammer uninstall <nombre> [--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 <attr> 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 <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. bwraplibcap, opensshzlib,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}).