gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ e5c47fd5b2b146a75a73a46c787bb9e75e4c687d docs/30-servicios-de-paquete.md Markdown · 172 líneas · 10.1 KB Historia crudo

SDD 30: los servicios de paquete — y el SDD 06 afirmaba un lector que no existe e5c47fd5b · Sergio ·

1# SDD 30 — Los servicios que trae un paquete
2
3> **Estado:** §3 IMPLEMENTADO (2026-09-12), §4 y §5 abiertos. Nace de una pregunta directa:
4> *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en
5> takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba,
6> qué se cerró y qué queda.
7>
8> Hermanos: [SDD 12](12-init-real.md) (arje como PID 1, contrato de la seed) ·
9> [SDD 06](06-swm-format.md) (la mutación `init_rule`) · `tawasuyu/03_ukupacha/arje/DE-SYSTEMD-A-ARJE.md`
10> (el manual de correspondencia de arje, autoritativo sobre el lado init).
11
12## 1. El hueco, medido
13
14Cuatro hallazgos, todos verificados en el árbol el 2026-09-12:
15
161. **La receta no tenía dónde declararlo.** `Recipe` era
17   `name, version, source, build, deps, evidence, slots, license, foreign`. Ninguna de las 979
18   recetas podía decir «yo traigo un demonio».
192. **Los dos servicios del producto vivían en constantes de Rust.**
20   `takana_bootstrap::STAGE1_SEED_CARD` (hammerd + console-getty) y `SSHD_SERVICE_CARD`, compuestas
21   por `product_seed_card()`. Añadir un tercero era editar Rust y recompilar takana.
223. **Los escritorios no usaban arje para esto en absoluto.** `scripts/gnome/gnome-start-qemu.sh`
23   lanza a mano `dbus-daemon --system --fork`, `arje-logind-compat &`, `accounts-daemon &`,
24   `upowerd &`, `pipewire &`, `pipewire-pulse &`, `wireplumber &` y `colord &`, con esperas de
25   socket por `sleep 0.25` en bucle; la imagen parchea la seed con python para que `console-getty`
26   ejecute `plasma-start` en vez de `/bin/sh`. Es **un card por IMAGEN, no por paquete**: esos ocho
27   demonios corren sin supervisión, sin backoff y sin el `CRASHED` real — justo lo que arje es PID 1
28   para dar.
294. **Las unidades upstream se sellan inertes.** 62 ficheros `.service` en el store (61 de usuario,
30   1 de sistema: `linux-pam`), que nadie lee ni traduce. Los `-Dsystemd=disabled` de las recetas son
31   por *dependencia*, no política de servicios.
32
33## 2. Por qué NO alcanza con `arje-absorb`
34
35arje **ya trae el traductor**: `init/arje-absorb`, con lectores de systemd, OpenRC, runit, dinit y
36sysvinit (`--from auto --root / --output seed.card.json`). Es la primera respuesta que uno da, y es
37la equivocada — por una razón exacta, escrita en su propio módulo de systemd:
38
39> *Se absorbe lo que está **habilitado**, no todo lo instalado: los symlinks de `<target>.wants/`.
40> Una unidad instalada pero no habilitada no arranca hoy, y meterla en el genesis la haría arrancar
41> mañana — cambiaría el sistema en vez de retratarlo.*
42
43`arje-absorb` **retrata un sistema vivo**. Un rootfs de takana no tiene ese estado que retratar:
44medido, hay **62 `.service` sellados y CERO directorios `.wants`** — nadie corrió nunca un
45`systemctl enable`, porque no hay systemd. Absorber un rootfs nuestro devuelve la lista vacía, y es
46la respuesta **correcta** a la pregunta que absorb contesta.
47
48⇒ **El «enable» de una distro construida desde fuente no se puede leer del árbol: hay que
49declararlo.** Ese es el trabajo que le toca a takana, y es el que faltaba.
50
51## 3. La decisión: declarar en dos mitades
52
53Se parte igual que systemd parte `[Service]` de `[Install]`, porque son dos hechos de dueño distinto:
54
55| hecho | dueño | dónde |
56|---|---|---|
57| **qué es** el servicio (exec, argv, supervisión, cgroup, red) | el PAQUETE | `[[service]]` en la receta ✅ |
58| **si arranca** en esta imagen | el PERFIL | `docs/state/targets.toml` ◑ (§4) |
59
60### 3.1 `[[service]]` en la receta (implementado)
61
62`crates/takana-core/src/service.rs`. Se traduce 1:1 a una Card de arje (`Service::card()`), con la
63forma del esquema validado de `card_core::Card`. Ejemplo real, `recipes/openssh.toml`:
64
65```toml
66[[service]]
67label      = "sshd"
68id         = "01HQAR53D4M2NBV8KZTYXFQA12"
69exec       = "/bin/busybox"
70argv       = ["sh", "-c", "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"]
71envp       = [["HOME", "/root"]]
72networking = "full"
73cgroup     = "arje.slice/sshd"
74restart    = { initial_ms = 500, max_ms = 20000 }
75```
76
77Tres propiedades que no son de estilo:
78
79- **Fuera de `hash_inputs`**, como `license`, `slots` y `evidence`: describe cómo se *supervisa* el
80  artefacto, no qué bytes tiene ⇒ se puebla en recetas YA SELLADAS sin mover un `ArtifactHash`.
81  **Medido**: el hash de openssh es `b3:938835e6…` con el binario viejo (que ignora el bloque) y el
82  mismo con el nuevo (que lo parsea y lo excluye). Si entrara al hash, declarar los servicios del
83  corpus costaría reconstruirlo entero y no se haría nunca.
84- **El `id` (ULID) se declara, no se genera.** Un id aleatorio haría que la seed —y con ella el
85  `product_rootfs_hash`— cambiara en cada corrida. La reproducibilidad del producto depende de que
86  esto sea estable, así que `validate()` lo exige y el de sshd es *el que la constante ya usaba*.
87- **Se valida al CARGAR la receta**, no al emitir la seed: un `id` mal puesto tiene que verse en la
88  receta, no tres pasos después en un rootfs que ya no reproduce y no dice por qué.
89
90**La prueba que autoriza el diseño** (`la_receta_de_openssh_reproduce_la_card_hardcodeada`): el card
91generado desde `recipes/openssh.toml` se compara **entero** contra `SSHD_SERVICE_CARD`, que es la
92card que el producto **ya bootea en QEMU** (e2e por `scripts/ssh-e2e-test.sh`). Empatan ⇒ mover el
93servicio de la constante a la receta no cambia un byte de la seed. No es «una card parecida»: es la
94misma.
95
96### 3.2 Los tres árboles que no se tocaban — decisión
97
98Había **tres convenciones de «dónde vive un servicio»**, ninguna conectada con las otras:
99
100| árbol | quién lo escribe | quién lo lee |
101|---|---|---|
102| `/ente/seed.card.json` (genesis) | `takana_bootstrap` | **arje-zero al boot** ✅ |
103| `/etc/arje/cards.d/*.json` | nadie | **arje, bajo demanda** (`ARJE_CARDS_DIR`) ✅ |
104| `/etc/hammer/init.d/<svc>.rule` | la mutación `init_rule` del `.swm` | **NADIE** ❌ |
105
106`docs/06-swm-format.md` afirma que «el init (arje) lee ese árbol». **No lo lee**: el único uso de
107`INIT_RULES_DIR` en todo el repo es escribirlo, y el contrato real de arje son los dos primeros.
108Para colmo `takana query service:<n>` busca en un CUARTO juego de rutas (`/etc/init.d/`,
109`/etc/service/<n>/run`).
110
111**Decisión:** los árboles canónicos son los de arje — **genesis de la seed** para lo que arranca al
112boot, **`cards.d/`** para lo que se levanta bajo demanda. `/etc/hammer/init.d/*.rule` queda
113**derogado**: o la mutación `init_rule` emite un card a `cards.d/`, o se retira del formato `.swm`.
114Es su propia unidad de trabajo (toca el esquema `.swm`, que es contrato firmado) y hasta entonces la
115afirmación del SDD 06 queda marcada como falsa aquí.
116
117## 4. Lo que queda: del card a la imagen
118
119**(a) El perfil habilita.** `targets.toml` lista raíces por perfil; falta el `services = [...]` que
120diga cuáles de los servicios declarados arrancan en esa imagen. Sin esto, «declarado» y «arranca»
121siguen siendo lo mismo, que es justo el error que `arje-absorb` evita.
122
123**(b) El emisor, y la trampa del sidecar rancio.** `seal_product_rootfs()` sirve a las dos vetas
124(`product` desde recetas locales, `product_from_repo` desde el repo firmado) y **sólo una tiene el
125directorio de recetas**. La fuente uniforme que ambas comparten es el **sidecar de provenance**
126`.hammer/recipe.toml`, que `takana-build` congela DENTRO del artefacto y `Store::recipe_for_hash`
127sabe leer.
128
129Pero ahí está la trampa, y es de la familia de «el cache-hit congela regresiones»: el sidecar
130**no entra en el `ArtifactHash`**, así que el openssh ya sellado en
131`store/938835e6…-openssh/.hammer/recipe.toml` **no tiene** el bloque `[[service]]` y, como el hash
132no se movió, `store.has()` da verdadero y **nunca se va a reconstruir solo**. Derivar la seed del
133sidecar hoy produciría un producto **sin sshd**, en silencio.
134
135⇒ La migración (re-sellar openssh para que su sidecar lleve el bloque) es una unidad de trabajo
136aparte y **con control**: hay que borrar el artefacto del store para forzar el rebuild, y openssh no
137está certificado como reproducible (su propia receta dice «el criterio es construye y corre»), así
138que el árbol nuevo hay que compararlo contra el viejo antes de darlo por bueno. Cambiar un artefacto
139que bootea en QEMU sin ese control es exactamente cómo se cuela una regresión que nadie ve.
140Hasta entonces `product_seed_card()` **sigue usando la constante**, a propósito y dicho acá.
141
142**(c) Los ocho de GNOME.** Convertir `gnome-start-qemu.sh` en cards es el caso que prueba el diseño
143de verdad, y depende de §5.
144
145## 5. El hueco que no es nuestro: no hay readiness
146
147arje ordena el arranque por **capacidades**, con orden topológico real
148(`arje-zero/src/graph/resolve.rs::plan_spawn`: punto fijo de satisfacibilidad + Kahn, detecta ciclos
149y descarta insatisfacibles). Pero eso ordena el **spawn**, no el **estar listo**: no existe una
150primitiva «esperá a que aparezca este socket». El propio manual de arje lo lista como hueco abierto
151(§13: *«readiness real de `Type=notify` — el shim registra pero no ordena»*).
152
153Es lo que el script de GNOME resuelve con `while [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; sleep 0.25`.
154Mientras el hueco siga abierto, un card que necesita esperar lo hace **dentro de su propio `argv`**
155con `sh -c '…; exec …'` — que es lo que ya hace el de sshd (levanta la red y genera las host keys
156antes del `exec`). Este diseño **no lo esconde**: lo deja a la vista en el `argv` en vez de meterlo
157en un wrapper, para que se vea cuántos servicios están pagando el hueco y valga como argumento río
158arriba en arje.
159
160## 6. Estado
161
162| pieza | estado |
163|---|---|
164| `[[service]]` en la receta + traducción a Card + validación | ✅ `crates/takana-core/src/service.rs` |
165| Fuera de `hash_inputs` (medido, hash de openssh sin mover) | ✅ |
166| Equivalencia receta → card hardcodeada de sshd | ✅ test en `takana-bootstrap` |
167| `openssh` declara su servicio | ✅ `recipes/openssh.toml` |
168| El perfil habilita (`targets.toml`) | ◑ §4a |
169| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) |
170| Los ocho demonios de GNOME como cards | ◻ §4c |
171| Derogar `/etc/hammer/init.d/*.rule` en el `.swm` | ◻ §3.2 |
172| Readiness por unidad en arje | ◻ §5 — río arriba |

30-servicios-de-paquete.md

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 (arje como PID 1, contrato de la seed) · SDD 06 (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:

hechodueñodónde
qué es el servicio (exec, argv, supervisión, cgroup, red)el PAQUETE[[service]] en la receta ✅
si arranca en esta imagenel PERFILdocs/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:

[[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:

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:

árbolquién lo escribequién lo lee
/ente/seed.card.json (genesis)takana_bootstraparje-zero al boot ✅
/etc/arje/cards.d/*.jsonnadiearje, bajo demanda (ARJE_CARDS_DIR) ✅
/etc/hammer/init.d/<svc>.rulela mutación init_rule del .swmNADIE ❌

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

piezaestado
[[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