1# SDD 30 — Los servicios que trae un paquete
2
3> **Estado:** §3 y §4a IMPLEMENTADOS (2026-09-12), §4b/§4c 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 — HECHO.** `servicios = [...]` por perfil en `targets.toml` (se hereda como
120`paquetes`), y `scripts/targets.py --services <perfil>` resuelve label → receta → exec cruzando las
121declaraciones del corpus con la membresía de perfil de los grafos de estado (los CINCO: el del
122corpus más los de cada cola, porque las recetas de un escritorio viven en `recipes/incoming-<x>/` y
123su membresía está en el grafo de SU cola).
124
125Lo que más valió no fue resolver, fue **la comprobación inversa**: avisar de los paquetes que están
126en la imagen, TRAEN un demonio y el perfil no arranca. Y encontró algo el primer día:
127
128> **`arje-logind-compat` y `arje-polkit-compat` estaban en CERO perfiles** — y sin embargo
129> `scripts/gnome/qemu-desktop-image.sh` los copia al rootfs a mano y el de COSMIC hace `exit 1` si
130> falta logind-compat. Dos binarios imprescindibles, presentes en la imagen y ausentes del destino
131> declarado. Es la misma forma del agujero de `foot`, encontrada esta vez por una comprobación en
132> vez de por una imagen inusable. Corregido: son raíces de `escritorio-gnome` (los dos) y de
133> `escritorio-cosmic` (sólo logind-compat; sus scripts no nombran polkit-compat).
134
135**Precedencia, y no es un detalle:** una RAÍZ del perfil está en el perfil por definición, aunque el
136grafo todavía no lo diga. `build-state.json` es DERIVADO de `targets.toml` y lo regenera el cron cada
13730 min; preguntarle primero al derivado haría que añadir una raíz se leyera como error hasta la
138siguiente cosecha — castigar al que arregla el manifiesto.
139
140El guardián se prueba solo: `scripts/targets.py --selftest` corre 7 casos —**el primero es el
141CONTROL, que tiene que pasar en verde**— incluidos «dos recetas del mismo perfil declaran el mismo
142label» (ERROR) y «el mismo label en dos colas distintas» (NO es colisión: `upower` existe
143legítimamente en incoming-gnome y en incoming-kde, y son la misma unidad en dos imágenes).
144
145**(b) El emisor, y la trampa del sidecar rancio.** `seal_product_rootfs()` sirve a las dos vetas
146(`product` desde recetas locales, `product_from_repo` desde el repo firmado) y **sólo una tiene el
147directorio de recetas**. La fuente uniforme que ambas comparten es el **sidecar de provenance**
148`.hammer/recipe.toml`, que `takana-build` congela DENTRO del artefacto y `Store::recipe_for_hash`
149sabe leer.
150
151Pero ahí está la trampa, y es de la familia de «el cache-hit congela regresiones»: el sidecar
152**no entra en el `ArtifactHash`**, así que el openssh ya sellado en
153`store/938835e6…-openssh/.hammer/recipe.toml` **no tiene** el bloque `[[service]]` y, como el hash
154no se movió, `store.has()` da verdadero y **nunca se va a reconstruir solo**. Derivar la seed del
155sidecar hoy produciría un producto **sin sshd**, en silencio.
156
157⇒ La migración (re-sellar openssh para que su sidecar lleve el bloque) es una unidad de trabajo
158aparte y **con control**: hay que borrar el artefacto del store para forzar el rebuild, y openssh no
159está certificado como reproducible (su propia receta dice «el criterio es construye y corre»), así
160que el árbol nuevo hay que compararlo contra el viejo antes de darlo por bueno. Cambiar un artefacto
161que bootea en QEMU sin ese control es exactamente cómo se cuela una regresión que nadie ve.
162Hasta entonces `product_seed_card()` **sigue usando la constante**, a propósito y dicho acá.
163
164**(c) Los de GNOME: DECLARADOS, no todavía arrancados por arje.** Las nueve unidades que
165`gnome-start-qemu.sh` levanta con `&` están declaradas en sus recetas y habilitadas en el perfil
166(`dbus-system`, `logind-compat`, `polkit-compat`, `accounts-daemon`, `upowerd`, `colord` de sistema;
167`pipewire`, `pipewire-pulse`, `wireplumber` de sesión). Verificado que ninguna movió su hash: 9/9
168idénticos a los que los grafos ya registraban.
169
170Dos cosas que NO son transcripción y hay que saber:
171
1721. **`dbus-daemon --fork` no se puede traducir tal cual.** arje supervisa al **hijo directo**:
173 `Type=forking` no existe en su modelo, así que un daemon que forkea y sale deja a arje viendo
174 morir al padre con éxito y reencarnándolo para siempre. La Card usa `--nofork --nopidfile`.
1752. **`scope = "system" | "session"`.** No es cosmético: decide DÓNDE va la Card. Las de sesión
176 necesitan `XDG_RUNTIME_DIR` y un usuario logueado, así que en el `genesis` arrancarían antes de
177 que exista ninguna de las dos cosas. Y su destino **no está resuelto fuera de mirada** — ahí las
178 arma el compositor (`mirada-compositor/src/session.rs`, `requires = [wayland_floor()]`, entregadas
179 a PID 1 por `RunCard`), pero mutter y kwin no tienen esa integración. `--services` avisa por cada
180 una: el hueco queda contado, no omitido.
181
182Falta el paso final: que el ensamblado de la imagen ponga las cards de sistema en el `genesis` en vez
183de que el script las lance con `&`. Eso ya es §4b (el emisor) y el arranque supervisado en QEMU.
184
185## 5. El hueco que no es nuestro: no hay readiness
186
187arje ordena el arranque por **capacidades**, con orden topológico real
188(`arje-zero/src/graph/resolve.rs::plan_spawn`: punto fijo de satisfacibilidad + Kahn, detecta ciclos
189y descarta insatisfacibles). Pero eso ordena el **spawn**, no el **estar listo**: no existe una
190primitiva «esperá a que aparezca este socket». El propio manual de arje lo lista como hueco abierto
191(§13: *«readiness real de `Type=notify` — el shim registra pero no ordena»*).
192
193Es lo que el script de GNOME resuelve con `while [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; sleep 0.25`.
194Mientras el hueco siga abierto, un card que necesita esperar lo hace **dentro de su propio `argv`**
195con `sh -c '…; exec …'` — que es lo que ya hace el de sshd (levanta la red y genera las host keys
196antes del `exec`). Este diseño **no lo esconde**: lo deja a la vista en el `argv` en vez de meterlo
197en un wrapper, para que se vea cuántos servicios están pagando el hueco y valga como argumento río
198arriba en arje.
199
200## 6. Estado
201
202| pieza | estado |
203|---|---|
204| `[[service]]` en la receta + traducción a Card + validación | ✅ `crates/takana-core/src/service.rs` |
205| Fuera de `hash_inputs` (medido, hash de openssh sin mover) | ✅ |
206| Equivalencia receta → card hardcodeada de sshd | ✅ test en `takana-bootstrap` |
207| `openssh` declara su servicio | ✅ `recipes/openssh.toml` |
208| El perfil habilita (`servicios` + `targets.py --services`) | ✅ §4a |
209| Guardián con roturas a propósito + control (`--selftest`) | ✅ 7/7 |
210| El vigía CORRE SOLO (cosecha-cron) → `docs/state/servicios.txt` | ✅ 0 errores, 18 avisos |
211| Los dos compat que no estaban en ningún perfil | ✅ corregido §4a |
212| Los 9 de GNOME declarados y habilitados (hash sin mover, 9/9) | ✅ §4c |
213| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) |
214| Que la imagen los arranque por arje y no por el script `&` | ◻ §4c, depende de §4b |
215| `escritorio-cosmic`: auditar su `cosmic-start.sh` y habilitar | ◻ hoy sale con 5 AVISO |
216
217**La primera lectura del vigía**, para que no haya que creerle a este documento: 0 errores y 18
218avisos. El más ruidoso es `dbus-system`, que viaja en **seis** perfiles y sólo GNOME lo arranca —
219que es exactamente el tipo de hecho que antes no tenía dónde verse.
220| Derogar `/etc/hammer/init.d/*.rule` en el `.swm` | ◻ §3.2 |
221| Readiness por unidad en arje | ◻ §5 — río arriba |
30-servicios-de-paquete.md
SDD 30 — Los servicios que trae un paquete
Estado: §3 y §4a IMPLEMENTADOS (2026-09-12), §4b/§4c 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:
- 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».
- 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.
- 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.
- 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:
[[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 — HECHO. servicios = [...] por perfil en targets.toml (se hereda como
paquetes), y scripts/targets.py --services <perfil> resuelve label → receta → exec cruzando las
declaraciones del corpus con la membresía de perfil de los grafos de estado (los CINCO: el del
corpus más los de cada cola, porque las recetas de un escritorio viven en recipes/incoming-<x>/ y
su membresía está en el grafo de SU cola).
Lo que más valió no fue resolver, fue la comprobación inversa: avisar de los paquetes que están
en la imagen, TRAEN un demonio y el perfil no arranca. Y encontró algo el primer día:
arje-logind-compat y arje-polkit-compat estaban en CERO perfiles — y sin embargo
scripts/gnome/qemu-desktop-image.sh los copia al rootfs a mano y el de COSMIC hace exit 1 si
falta logind-compat. Dos binarios imprescindibles, presentes en la imagen y ausentes del destino
declarado. Es la misma forma del agujero de foot, encontrada esta vez por una comprobación en
vez de por una imagen inusable. Corregido: son raíces de escritorio-gnome (los dos) y de
escritorio-cosmic (sólo logind-compat; sus scripts no nombran polkit-compat).
Precedencia, y no es un detalle: una RAÍZ del perfil está en el perfil por definición, aunque el
grafo todavía no lo diga. build-state.json es DERIVADO de targets.toml y lo regenera el cron cada
30 min; preguntarle primero al derivado haría que añadir una raíz se leyera como error hasta la
siguiente cosecha — castigar al que arregla el manifiesto.
El guardián se prueba solo: scripts/targets.py --selftest corre 7 casos —el primero es el
CONTROL, que tiene que pasar en verde— incluidos «dos recetas del mismo perfil declaran el mismo
label» (ERROR) y «el mismo label en dos colas distintas» (NO es colisión: upower existe
legítimamente en incoming-gnome y en incoming-kde, y son la misma unidad en dos imágenes).
(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 de GNOME: DECLARADOS, no todavía arrancados por arje. Las nueve unidades que
gnome-start-qemu.sh levanta con & están declaradas en sus recetas y habilitadas en el perfil
(dbus-system, logind-compat, polkit-compat, accounts-daemon, upowerd, colord de sistema;
pipewire, pipewire-pulse, wireplumber de sesión). Verificado que ninguna movió su hash: 9/9
idénticos a los que los grafos ya registraban.
Dos cosas que NO son transcripción y hay que saber:
dbus-daemon --fork no se puede traducir tal cual. arje supervisa al hijo directo:
Type=forking no existe en su modelo, así que un daemon que forkea y sale deja a arje viendo
morir al padre con éxito y reencarnándolo para siempre. La Card usa --nofork --nopidfile.
scope = "system" | "session". No es cosmético: decide DÓNDE va la Card. Las de sesión
necesitan XDG_RUNTIME_DIR y un usuario logueado, así que en el genesis arrancarían antes de
que exista ninguna de las dos cosas. Y su destino no está resuelto fuera de mirada — ahí las
arma el compositor (mirada-compositor/src/session.rs, requires = [wayland_floor()], entregadas
a PID 1 por RunCard), pero mutter y kwin no tienen esa integración. --services avisa por cada
una: el hueco queda contado, no omitido.
Falta el paso final: que el ensamblado de la imagen ponga las cards de sistema en el genesis en vez
de que el script las lance con &. Eso ya es §4b (el emisor) y el arranque supervisado en QEMU.
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 (servicios + targets.py --services) | ✅ §4a |
Guardián con roturas a propósito + control (--selftest) | ✅ 7/7 |
El vigía CORRE SOLO (cosecha-cron) → docs/state/servicios.txt | ✅ 0 errores, 18 avisos |
| Los dos compat que no estaban en ningún perfil | ✅ corregido §4a |
| Los 9 de GNOME declarados y habilitados (hash sin mover, 9/9) | ✅ §4c |
| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) |
Que la imagen los arranque por arje y no por el script & | ◻ §4c, depende de §4b |
escritorio-cosmic: auditar su cosmic-start.sh y habilitar | ◻ hoy sale con 5 AVISO |
La primera lectura del vigía, para que no haya que creerle a este documento: 0 errores y 18
avisos. El más ruidoso es dbus-system, que viaja en seis perfiles y sólo GNOME lo arranca —
que es exactamente el tipo de hecho que antes no tenía dónde verse.
| Derogar /etc/hammer/init.d/*.rule en el .swm | ◻ §3.2 |
| Readiness por unidad en arje | ◻ §5 — río arriba |