SDD 30: los servicios de paquete — y el SDD 06 afirmaba un lector que no existe
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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 `<target>.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/<svc>.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:<n>` busca en un CUARTO juego de rutas (`/etc/init.d/`,
|
||||
`/etc/service/<n>/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 |
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user