El diseño completo del hueco: qué declara el PAQUETE (`[[service]]`, hecho) y qué decide el PERFIL (si arranca, pendiente), que es la misma partición que systemd hace entre [Service] e [Install] y que `arje-absorb` respeta al absorber sólo lo habilitado. Deja escritas las dos cosas que cuestan caro si se descubren después: 1. El SDD 06 decía «el init (arje) lee ese árbol» de /etc/hammer/init.d/*.rule. Es FALSO: el único uso de INIT_RULES_DIR en el repo es escribirlo. Había TRES convenciones de dónde vive un servicio y ninguna se tocaba con las otras (más una cuarta en `query service:`). Canónicos son los de arje —genesis de la seed y cards.d/—; la mutación del .swm queda derogada o reapuntada, y la afirmación falsa queda marcada en su propio doc. 2. La trampa para el emisor: el sidecar .hammer/recipe.toml NO entra al ArtifactHash, así que el openssh ya sellado no lleva el bloque y, con el hash sin mover, NUNCA se reconstruye solo. Derivar la seed del sidecar hoy daría un producto SIN sshd, en silencio. Por eso product_seed_card() sigue usando la constante a propósito, y re-sellar es una unidad aparte CON control de reproducibilidad — openssh no está certificado como reproducible.
194 lines
11 KiB
Markdown
194 lines
11 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]` — lista el catálogo (`<repo>/index.json`).
|
|
- `takana repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras
|
|
publicar (cada `pack --repo` invalida la firma del release).
|
|
- `takana repo verify --repo DIR [--trust DIR]` — verifica la firma del release.
|
|
- `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}`).
|