Files
takana/docs/06-swm-format.md
Sergio e0e390c282 repo list/verify: hablan HTTP — un repo remoto con 173 paquetes se leía como vacío
`repo list --repo <url>` tomaba un `PathBuf`, así que trataba la URL como una ruta
relativa que no existe y respondía **«repo vacío» con salida 0**. No es que no
soportara el caso: es que afirmaba lo contrario de lo que pasaba, y con rc=0, que
es la forma de fallo que un script no puede detectar. Salió al probar el repo del
dominio nuevo.

`install --repo` ya sabía hablar HTTP con `RepoSource` (ADR 0014: lista de
orígenes separada por comas, probados en orden). `list` y `verify` ahora usan el
MISMO resolvedor, así que la misma cadena de espejos vale en los tres verbos.
`sign` se queda local a propósito: firmar es escribir.

Y `verify` remoto no es un extra — es el caso de uso del ADR 0014. Verificar el
release de un espejo ANTES de instalarle nada era imposible sin construir.

De paso, tres estados que se daban por iguales y ahora se distinguen (CLAUDE.md
§3: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo
fue bien):

  dir que NO EXISTE        → error, rc=1, y sugiere que quizá era una URL
  dir sin index.json       → «repo por crear» (estado válido: lo crea `pack --repo`)
  índice con 0 paquetes    → «índice publicado y SIN paquetes»
  origen que no sirve      → error del fetch, con los orígenes probados

Y el índice se atribuye al origen que REALMENTE lo sirvió, no a la lista entera:
con `<espejo-caído>,<bueno>` la salida dice el bueno. Es la misma regla que el
propio `fetch_desde_algun_origen` aplica a los `.swm` —«decir bajado de la lista
entera cuando sólo uno respondió es una media verdad»— que `read_index` tiraba.

Medido contra el repo vivo: `list` 173 paquetes [release firmado por release],
`verify` trusted. 93+93+6+1+3+4 tests del crate en verde.
2026-09-21 21:28:49 +00:00

201 lines
12 KiB
Markdown

# SDD 06 — Formato `.swm` (Software Mutación)
El `.swm` es la unidad de intercambio de `takana`. **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 takana
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 → `takana 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» era FALSO** — verificado el 2026-09-12: el único uso de `INIT_RULES_DIR` en todo el repo es ESCRIBIRLO, y el contrato real de arje son `/ente/seed.card.json` y `/etc/arje/cards.d/*.json`. Esta mutación es hoy **write-only**: derogarla o reapuntarla a `cards.d/` es [SDD 30 §3.2](30-servicios-de-paquete.md) |
| `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 (`takana apply`)
```
takana 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 → `takana commit`. Si mal → `takana 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 `takana` 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
// takana-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:
- `takana 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.
- `takana swm-verify <file.swm>` — chequea schema, hashes de los `file_drop` inline y,
con `--base-ref`, la compatibilidad de base.
- `takana 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.)
- `takana 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`.
- `takana 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).
- `takana repo list [--repo DIR|URL]` — lista el catálogo (`<repo>/index.json`). Desde el
2026-09-21 acepta lo MISMO que `install --repo`: un directorio o una lista de orígenes HTTP(S)
separada por comas. Antes sólo miraba directorios y ante una URL respondía **«vacío» con salida
0** — un repo remoto con 173 paquetes se leía como uno sin nada. Y ahora distingue «todavía no
hay `index.json`» (repo por crear) de «índice publicado y sin paquetes», que también se daban
por iguales.
- `takana repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras
publicar (cada `pack --repo` invalida la firma del release). **Sólo local:** firmar es escribir.
- `takana repo verify --repo DIR|URL [--trust DIR]` — verifica la firma del release. También
acepta orígenes HTTP(S): verificar el release de un espejo ANTES de instarle nada es justo el
caso de uso del ADR 0014, y hasta el 2026-09-21 no había forma de hacerlo sin construir.
- `takana 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.
- `takana 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).
- `takana 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 takana. Trae la RECETA (source+hash+deps+flags), NUNCA el binario del cache de nix —
takana 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
takana (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 `takana-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.
**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}`).