diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md index 87b38639..d8c92616 100644 --- a/docs/06-swm-format.md +++ b/docs/06-swm-format.md @@ -57,7 +57,7 @@ signature: # opcional pero recomendado (ver SDD 09) |---|---|---| | `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 | +| `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: diff --git a/docs/30-servicios-de-paquete.md b/docs/30-servicios-de-paquete.md new file mode 100644 index 00000000..2eb8d092 --- /dev/null +++ b/docs/30-servicios-de-paquete.md @@ -0,0 +1,172 @@ +# SDD 30 — Los servicios que trae un paquete + +> **Estado:** §3 IMPLEMENTADO (2026-09-12), §4 y §5 abiertos. Nace de una pregunta directa: +> *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en +> takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba, +> qué se cerró y qué queda. +> +> Hermanos: [SDD 12](12-init-real.md) (arje como PID 1, contrato de la seed) · +> [SDD 06](06-swm-format.md) (la mutación `init_rule`) · `tawasuyu/03_ukupacha/arje/DE-SYSTEMD-A-ARJE.md` +> (el manual de correspondencia de arje, autoritativo sobre el lado init). + +## 1. El hueco, medido + +Cuatro hallazgos, todos verificados en el árbol el 2026-09-12: + +1. **La receta no tenía dónde declararlo.** `Recipe` era + `name, version, source, build, deps, evidence, slots, license, foreign`. Ninguna de las 979 + recetas podía decir «yo traigo un demonio». +2. **Los dos servicios del producto vivían en constantes de Rust.** + `takana_bootstrap::STAGE1_SEED_CARD` (hammerd + console-getty) y `SSHD_SERVICE_CARD`, compuestas + por `product_seed_card()`. Añadir un tercero era editar Rust y recompilar takana. +3. **Los escritorios no usaban arje para esto en absoluto.** `scripts/gnome/gnome-start-qemu.sh` + lanza a mano `dbus-daemon --system --fork`, `arje-logind-compat &`, `accounts-daemon &`, + `upowerd &`, `pipewire &`, `pipewire-pulse &`, `wireplumber &` y `colord &`, con esperas de + socket por `sleep 0.25` en bucle; la imagen parchea la seed con python para que `console-getty` + ejecute `plasma-start` en vez de `/bin/sh`. Es **un card por IMAGEN, no por paquete**: esos ocho + demonios corren sin supervisión, sin backoff y sin el `CRASHED` real — justo lo que arje es PID 1 + para dar. +4. **Las unidades upstream se sellan inertes.** 62 ficheros `.service` en el store (61 de usuario, + 1 de sistema: `linux-pam`), que nadie lee ni traduce. Los `-Dsystemd=disabled` de las recetas son + por *dependencia*, no política de servicios. + +## 2. Por qué NO alcanza con `arje-absorb` + +arje **ya trae el traductor**: `init/arje-absorb`, con lectores de systemd, OpenRC, runit, dinit y +sysvinit (`--from auto --root / --output seed.card.json`). Es la primera respuesta que uno da, y es +la equivocada — por una razón exacta, escrita en su propio módulo de systemd: + +> *Se absorbe lo que está **habilitado**, no todo lo instalado: los symlinks de `.wants/`. +> Una unidad instalada pero no habilitada no arranca hoy, y meterla en el genesis la haría arrancar +> mañana — cambiaría el sistema en vez de retratarlo.* + +`arje-absorb` **retrata un sistema vivo**. Un rootfs de takana no tiene ese estado que retratar: +medido, hay **62 `.service` sellados y CERO directorios `.wants`** — nadie corrió nunca un +`systemctl enable`, porque no hay systemd. Absorber un rootfs nuestro devuelve la lista vacía, y es +la respuesta **correcta** a la pregunta que absorb contesta. + +⇒ **El «enable» de una distro construida desde fuente no se puede leer del árbol: hay que +declararlo.** Ese es el trabajo que le toca a takana, y es el que faltaba. + +## 3. La decisión: declarar en dos mitades + +Se parte igual que systemd parte `[Service]` de `[Install]`, porque son dos hechos de dueño distinto: + +| hecho | dueño | dónde | +|---|---|---| +| **qué es** el servicio (exec, argv, supervisión, cgroup, red) | el PAQUETE | `[[service]]` en la receta ✅ | +| **si arranca** en esta imagen | el PERFIL | `docs/state/targets.toml` ◑ (§4) | + +### 3.1 `[[service]]` en la receta (implementado) + +`crates/takana-core/src/service.rs`. Se traduce 1:1 a una Card de arje (`Service::card()`), con la +forma del esquema validado de `card_core::Card`. Ejemplo real, `recipes/openssh.toml`: + +```toml +[[service]] +label = "sshd" +id = "01HQAR53D4M2NBV8KZTYXFQA12" +exec = "/bin/busybox" +argv = ["sh", "-c", "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"] +envp = [["HOME", "/root"]] +networking = "full" +cgroup = "arje.slice/sshd" +restart = { initial_ms = 500, max_ms = 20000 } +``` + +Tres propiedades que no son de estilo: + +- **Fuera de `hash_inputs`**, como `license`, `slots` y `evidence`: describe cómo se *supervisa* el + artefacto, no qué bytes tiene ⇒ se puebla en recetas YA SELLADAS sin mover un `ArtifactHash`. + **Medido**: el hash de openssh es `b3:938835e6…` con el binario viejo (que ignora el bloque) y el + mismo con el nuevo (que lo parsea y lo excluye). Si entrara al hash, declarar los servicios del + corpus costaría reconstruirlo entero y no se haría nunca. +- **El `id` (ULID) se declara, no se genera.** Un id aleatorio haría que la seed —y con ella el + `product_rootfs_hash`— cambiara en cada corrida. La reproducibilidad del producto depende de que + esto sea estable, así que `validate()` lo exige y el de sshd es *el que la constante ya usaba*. +- **Se valida al CARGAR la receta**, no al emitir la seed: un `id` mal puesto tiene que verse en la + receta, no tres pasos después en un rootfs que ya no reproduce y no dice por qué. + +**La prueba que autoriza el diseño** (`la_receta_de_openssh_reproduce_la_card_hardcodeada`): el card +generado desde `recipes/openssh.toml` se compara **entero** contra `SSHD_SERVICE_CARD`, que es la +card que el producto **ya bootea en QEMU** (e2e por `scripts/ssh-e2e-test.sh`). Empatan ⇒ mover el +servicio de la constante a la receta no cambia un byte de la seed. No es «una card parecida»: es la +misma. + +### 3.2 Los tres árboles que no se tocaban — decisión + +Había **tres convenciones de «dónde vive un servicio»**, ninguna conectada con las otras: + +| árbol | quién lo escribe | quién lo lee | +|---|---|---| +| `/ente/seed.card.json` (genesis) | `takana_bootstrap` | **arje-zero al boot** ✅ | +| `/etc/arje/cards.d/*.json` | nadie | **arje, bajo demanda** (`ARJE_CARDS_DIR`) ✅ | +| `/etc/hammer/init.d/.rule` | la mutación `init_rule` del `.swm` | **NADIE** ❌ | + +`docs/06-swm-format.md` afirma que «el init (arje) lee ese árbol». **No lo lee**: el único uso de +`INIT_RULES_DIR` en todo el repo es escribirlo, y el contrato real de arje son los dos primeros. +Para colmo `takana query service:` busca en un CUARTO juego de rutas (`/etc/init.d/`, +`/etc/service//run`). + +**Decisión:** los árboles canónicos son los de arje — **genesis de la seed** para lo que arranca al +boot, **`cards.d/`** para lo que se levanta bajo demanda. `/etc/hammer/init.d/*.rule` queda +**derogado**: o la mutación `init_rule` emite un card a `cards.d/`, o se retira del formato `.swm`. +Es su propia unidad de trabajo (toca el esquema `.swm`, que es contrato firmado) y hasta entonces la +afirmación del SDD 06 queda marcada como falsa aquí. + +## 4. Lo que queda: del card a la imagen + +**(a) El perfil habilita.** `targets.toml` lista raíces por perfil; falta el `services = [...]` que +diga cuáles de los servicios declarados arrancan en esa imagen. Sin esto, «declarado» y «arranca» +siguen siendo lo mismo, que es justo el error que `arje-absorb` evita. + +**(b) El emisor, y la trampa del sidecar rancio.** `seal_product_rootfs()` sirve a las dos vetas +(`product` desde recetas locales, `product_from_repo` desde el repo firmado) y **sólo una tiene el +directorio de recetas**. La fuente uniforme que ambas comparten es el **sidecar de provenance** +`.hammer/recipe.toml`, que `takana-build` congela DENTRO del artefacto y `Store::recipe_for_hash` +sabe leer. + +Pero ahí está la trampa, y es de la familia de «el cache-hit congela regresiones»: el sidecar +**no entra en el `ArtifactHash`**, así que el openssh ya sellado en +`store/938835e6…-openssh/.hammer/recipe.toml` **no tiene** el bloque `[[service]]` y, como el hash +no se movió, `store.has()` da verdadero y **nunca se va a reconstruir solo**. Derivar la seed del +sidecar hoy produciría un producto **sin sshd**, en silencio. + +⇒ La migración (re-sellar openssh para que su sidecar lleve el bloque) es una unidad de trabajo +aparte y **con control**: hay que borrar el artefacto del store para forzar el rebuild, y openssh no +está certificado como reproducible (su propia receta dice «el criterio es construye y corre»), así +que el árbol nuevo hay que compararlo contra el viejo antes de darlo por bueno. Cambiar un artefacto +que bootea en QEMU sin ese control es exactamente cómo se cuela una regresión que nadie ve. +Hasta entonces `product_seed_card()` **sigue usando la constante**, a propósito y dicho acá. + +**(c) Los ocho de GNOME.** Convertir `gnome-start-qemu.sh` en cards es el caso que prueba el diseño +de verdad, y depende de §5. + +## 5. El hueco que no es nuestro: no hay readiness + +arje ordena el arranque por **capacidades**, con orden topológico real +(`arje-zero/src/graph/resolve.rs::plan_spawn`: punto fijo de satisfacibilidad + Kahn, detecta ciclos +y descarta insatisfacibles). Pero eso ordena el **spawn**, no el **estar listo**: no existe una +primitiva «esperá a que aparezca este socket». El propio manual de arje lo lista como hueco abierto +(§13: *«readiness real de `Type=notify` — el shim registra pero no ordena»*). + +Es lo que el script de GNOME resuelve con `while [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; sleep 0.25`. +Mientras el hueco siga abierto, un card que necesita esperar lo hace **dentro de su propio `argv`** +con `sh -c '…; exec …'` — que es lo que ya hace el de sshd (levanta la red y genera las host keys +antes del `exec`). Este diseño **no lo esconde**: lo deja a la vista en el `argv` en vez de meterlo +en un wrapper, para que se vea cuántos servicios están pagando el hueco y valga como argumento río +arriba en arje. + +## 6. Estado + +| pieza | estado | +|---|---| +| `[[service]]` en la receta + traducción a Card + validación | ✅ `crates/takana-core/src/service.rs` | +| Fuera de `hash_inputs` (medido, hash de openssh sin mover) | ✅ | +| Equivalencia receta → card hardcodeada de sshd | ✅ test en `takana-bootstrap` | +| `openssh` declara su servicio | ✅ `recipes/openssh.toml` | +| El perfil habilita (`targets.toml`) | ◑ §4a | +| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) | +| Los ocho demonios de GNOME como cards | ◻ §4c | +| Derogar `/etc/hammer/init.d/*.rule` en el `.swm` | ◻ §3.2 | +| Readiness por unidad en arje | ◻ §5 — río arriba | diff --git a/docs/README.md b/docs/README.md index f1d3a37b..53b809b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,7 @@ discrepancia, o se corrige el código o se actualiza el SDD con un commit que ex | 12 | [`arje` como init real del Stage 1](12-init-real.md) | Contrato de runtime: seed card, hammerd supervisado, `CRASHED` real | | 16 | [harkaq: la jaula de takana](16-harkaq-jaula.md) | Landlock+seccomp sobre el bwrap actual; política = clausura; evidencia negativa de hermeticidad | | 26 | [`atuq`: el envoltorio Gecko](26-atuq-envoltorio-gecko.md) | Navegador propio como artefacto DERIVADO de `firefox` (no fork de fuente); la toolchain clang como puerta de PGO/LTO; qué se promete y qué no | +| 30 | [Los servicios que trae un paquete](30-servicios-de-paquete.md) | `[[service]]` en la receta → Card de arje; por qué `arje-absorb` no cubre una distro construida desde fuente; los tres árboles de servicios y cuál es el canónico | ### Runbooks (operativos)