# SDD 30 — Los servicios que trae un paquete > **Estado:** §3, §4a y §4b IMPLEMENTADOS y **VALIDADOS EN ARRANQUE REAL** (2026-09-12/14); > §4c CERRADO también para GNOME (2026-09-14, con control); §5 abierto. 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 — HECHO.** `servicios = [...]` por perfil en `targets.toml` (se hereda como `paquetes`), y `scripts/targets.py --services ` 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-/` 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 — HECHO, y la trampa quedó armada en un test.** `service_cards()` lee la receta que viaja DENTRO de cada artefacto sellado (el sidecar de provenance `.hammer/recipe.toml`, vía `Store::recipe_for_hash`) y filtra `scope = "system"`. Se lee de ahí y no de `recipes/` porque `seal_product_rootfs()` sirve a las DOS vetas —`product` desde recetas locales y `product_from_repo` desde el repo firmado— y **sólo una tiene `recipes/` a mano**: el sidecar es la única fuente que ambas comparten, que es lo que sostiene que las dos converjan al mismo `product-rootfs`. La trampa era real y sigue ahí: **el sidecar no entra en el `ArtifactHash`**, así que un artefacto sellado antes de que su receta declarara servicios es un cache-hit perfectamente válido y no se reconstruye nunca solo. Si el emisor devolviera lista vacía sin protestar, el producto saldría **booteando, verde y sin sshd**. Por eso `service_cards()` **falla ruidosamente** con un mensaje que explica la causa y da el arreglo, y hay un test (`service_cards_falla_ruidosamente_con_sidecar_rancio`) que exige que el mensaje siga diciendo las dos cosas. Es la regla 3 del CLAUDE.md aplicada antes de pagarla: un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien. ### 4b.1 La migración de openssh, medida — y un hecho nuevo Re-sellar openssh era el paso que faltaba, y se hizo **con control** porque openssh no estaba certificado como reproducible (su propia receta dice «el criterio es construye y corre»). El procedimiento, por si hay que repetirlo con otro componente de servicio: 1. **Manifiesto de control ANTES de tocar nada**: `sha256` + modo de los 33 ficheros. 2. **Respaldo por hardlink** — y acá saltó el gotcha conocido: `store/` es un bind-mount de `/dev/sdb` y `work/` vive en `/dev/sdc`, así que `cp -al` a `work/` muere con `Invalid cross-device link`. El respaldo tiene que ir **dentro del mount del store**; `store/.respaldo-…` sirve y es invisible para `store-gc.sh`, que sólo considera dirs `^[0-9a-f]{64}-`. 3. Borrar el artefacto (los bytes sobreviven por el respaldo: 11 enlaces → 10) y reconstruir bajo `flock -o`. 4. Comparar árbol nuevo contra el manifiesto. **Resultado: openssh REPRODUCE BIT A BIT.** 33/33 ficheros, modos idénticos, y la única diferencia en todo el árbol es `.hammer/recipe.toml` — exactamente el sidecar que se quería migrar y nada más. Eso es un hecho nuevo sobre el corpus, no sólo un paso de esta campaña: openssh pasa de «no certificado» a «reproduce», comprobado contra un control tomado antes del borrado. Coste que conviene saber: los árboles ya hidratados compartían inodes con el artefacto viejo, y el nuevo tiene inodes propios (`%h = 1`). El contenido es el mismo, pero hasta que esos árboles se refresquen el disco lleva dos copias. ### 4b.2 La cadena entera, probada en un arranque real No alcanza con que los tests pasen: el punto de todo esto es que **un servicio declarado en una receta termine supervisado por PID 1**. Medido el 2026-09-14, `scripts/product-boot-test.sh` sobre el `product-rootfs` ensamblado por la ruta real: ``` ok card sshd en la seed ==> boot QEMU (mem 2048), hostfwd :2223->:22 PRODUCT_SSH_OK uid=0 ``` La cadena completa, sin ningún eslabón escrito a mano: recipes/openssh.toml `[[service]]` → sidecar `.hammer/recipe.toml` DENTRO del artefacto sellado → `service_cards()` (scope = system) → `genesis` de `/ente/seed.card.json` → arje-zero encarna sshd al boot → la sesión SSH responde **Y el hash no se movió por esto.** El `product-rootfs` salió con hash distinto al anterior (`5011955a…` → `8639894332…`), y había que saber si lo movía este cambio: **no**. La seed de ambos árboles es **byte-idéntica** (`diff` sobre el JSON: sin diferencias); lo que cambió fueron `netup` —otro artefacto que cuando se selló el viejo— y, por arrastre, `ente/attest.json`, que registra su BLAKE3. La constante y la receta producen exactamente la misma seed, comprobado sobre el árbol real y no sólo en un test. **(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: 1. **`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`. 2. **`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. #### 4c.1 GNOME con los demonios supervisados por arje — medido contra un control El bloqueo que este documento daba por corpus (gnome-shell en deuda) **se levantó solo**: otro agente selló `evolution-data-server` y `gnome-shell` mientras esto se escribía. `escritorio-gnome` quedó 309/309 y el cierre hidrata completo (319 nodos, 8,2 G). **Se corrió un CONTROL primero**, y fue lo que hizo interpretable el resultado: la misma imagen sin inyectar cards, para saber si GNOME arranca hoy en este hub. Arranca — y trae un fallo propio que sirvió de sonda. | | control (el script con `&`) | con Cards en el `genesis` | |---|---|---| | `colord` | `!! colord MURIÓ al arrancar` | `ya vive (pid 175)` — lo encarnó PID 1 | | `org.freedesktop.ColorManager` | `NO apareció en 40s` | **`OK`** | | `login1` / `Accounts` / `UPower` | OK | OK | | compositor | `wayland-0`, shell vivo | `wayland-0`, shell vivo | **El fallo del control desapareció.** No es una mejora buscada: `colord` moría arrancado por el script y vive arrancado por arje, con el bus ya listo porque la espera está dentro de su `argv`. Es exactamente lo que un supervisor está para dar, y el control es lo único que permite decirlo — sin él, «ColorManager OK» sería un dato suelto en vez de una diferencia. Los PIDs lo confirman: `polkit-compat`=119, `colord`=175, `upowerd`=178, `accounts-daemon`=180 — números bajos, de antes de que el lanzador de sesión existiera. **Qué NO se comprobó:** no hay `screendump`. QEMU salió por timeout antes de poder pedirlo, y el control tampoco lo tuvo, así que la comparación es serial contra serial. Lo que se afirma acá es sobre los DEMONIOS (nombres del bus, PIDs, supervisión), no sobre el pintado; para afirmar que la imagen pinta sigue rigiendo la regla de validar escritorios con pantalla. #### Las piezas, y por qué cada una está donde está - **`takana service-cards `** — la traducción `[[service]]` → Card, con **una sola implementación**. El script de imagen ya parchea la seed con python; si además compusiera el JSON de la Card habría dos generadores del mismo formato divergiendo en silencio, y el formato es contrato con `card_core::Card`, que no es nuestro. - **`targets.py --service-paths `** — une las dos mitades: qué es el servicio (receta) y si arranca (perfil). Sólo `scope = system`. - **`scripts/gnome/inyectar-cards.py`** — compone la seed, idempotente por `label`. Es un fichero y no un heredoc porque **el script de imagen ya tiene uno de python y anidar dos falló en vivo**: el terminador del interno cierra el externo y media cosa se ejecuta como shell. - **La espera del bus dentro del `argv`** de las cinco recetas de sistema. Sin ella un daemon puede arrancar antes de que dbus escuche y quedarse en modo idle sin registrar su nombre — un fallo que **no se ve**, porque el proceso vive y el bus no lo tiene. Los 5 hashes no se movieron. - **El guardia de `gnome-start` es por «¿está corriendo?», no por una perilla de la imagen.** Así es correcto venga de donde venga el proceso, y la misma copia sirve en una imagen sin cards. Una condición que consulta el estado real no puede desincronizarse de él; un flag sí. #### Lo que este frente se encontró, no lo que se propuso `--service-paths` devolvió **nueve** servicios de sistema y sólo seis eran míos: otro agente declaró `cupsd`, `bluetoothd` y `NetworkManager` en sus recetas y los habilitó en el perfil, con `-f`, `--nodetach` y `--no-daemon` — o sea que leyó y aplicó la regla de que arje supervisa al hijo directo y `Type=forking` no existe. El mecanismo se usó sin que nadie lo coordinara, que es la única prueba que vale de que el sitio donde se puso la declaración era el correcto. #### Lo que queda de §4c La base de la imagen es el `product-rootfs`, no `work/metal-rootfs`: ese fichero **no existe en este hub y ningún script del repo lo produce**. El producto sirve porque ya es arje-zero PID 1 + seed + busybox, pero el andamiaje de imagen de escritorio sigue dependiendo de un insumo sin dueño, y eso es su propia deuda. Y las tres cosas de siempre: EXDEV. `store/` es bind-mount de `/dev/sdb` y `work/` vive en `/dev/sdc`, así que **todo lo que enlaza en vez de copiar** se rompe al cruzar — lo pegaron `hydrate-profile.py` (que ya lo detecta y dice dónde poner el `--into`) y el staging de `install-image-efi.sh` (configurable por `STAGE`). Hidratado, fundido y staging tienen que vivir bajo el mismo montaje. ## 4d. La otra mitad, que faltaba: `[[user]]` *(2026-09-14)* Declarar cómo se supervisa un demonio no alcanza cuando el demonio **se niega a correr como root**. `gitea` lo dice y sale: [F] Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission. Su Card hace `setuidgid gitea`, y sin la cuenta el servicio arranca, muere y reintenta para siempre con un log que dice `unknown user` — no «a la imagen le falta una cuenta». El `/etc/passwd` de la imagen era **la constante `takana_bootstrap::PRODUCT_PASSWD`** (`root` y `sshd`, nada más): añadir un usuario era editar Rust y recompilar takana, que es **exactamente el hueco del §1 de este documento**, resuelto para las Cards y dejado abierto para las cuentas. Simetría, no elegancia. ```toml [[user]] name = "gitea" uid = 916 home = "/var/lib/gitea" ``` **El `uid` se declara, no se asigna**, por la misma razón que el ULID de la Card: un «primero libre a partir de 1000» hace que dos imágenes del mismo perfil salgan con dueños distintos y el rootfs **deje de reproducir sin que nada falle** — los ficheros se ven iguales y `ls -l` dice otro número. Fuera de `hash_inputs`, como `[[service]]`: medido, el hash de `gitea` no se movió al declararlo. ### La pregunta que NO es la misma que la de los servicios `--service-paths` lista las recetas de los servicios **habilitados**; `--user-paths` lista las de los paquetes **instalados** que declaran cuentas. No es el mismo conjunto y confundirlos rompe el caso normal: `postgres` instalado y sin levantar necesita su usuario igual, porque los ficheros que ya están en la imagen **ya son suyos**. ### Fusionar, no escribir `takana users --merge ` fusiona **por clave, no por línea entera**: componer la imagen dos veces no deja la cuenta dos veces (y `grep -q` sobre la línea completa no serviría — la misma cuenta con el GECOS cambiado se leería como nueva). Tres decisiones, las tres con su control: 1. **Una cuenta ya presente con OTRA línea es un conflicto y no se pisa**: sale ≠0. Pisarla es cambiarle el uid a ficheros que ya son de alguien, y eso se descubre dentro de la VM. 2. **Se planea todo y sólo entonces se escribe.** Fichero a fichero, un conflicto en `passwd` dejaría el `group` ya escrito: media cuenta es peor que ninguna, porque parece que está. 3. **Un `passwd` ausente es un error, no un fichero a crear.** Crearlo dejaría una imagen **sin `root`** y con el usuario del paquete. Y la validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids (los reserva el producto), uid fuera de `100..=65533`, `home` relativo, y un `:` en cualquier campo — que partiría la línea en dos y fallaría lejos de acá. **Verificado**: 6 tests del módulo, merge idempotente (segunda pasada `+0`), y los dos controles negativos con el fichero **intacto** después. `scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen. ## 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 (`service_cards`, falla ruidosa + test) | ✅ §4b | | Re-sellado de openssh con control → **reproduce bit a bit** | ✅ §4b.1 | | La cadena receta→sidecar→card→genesis→arje, **en arranque real** | ✅ §4b.2 (`PRODUCT_SSH_OK`) | | Que la imagen de ESCRITORIO los arranque y no el script `&` | ✅ §4c.1, con control (y arregló `colord`) | | `takana service-cards` + `--service-paths` + el inyector | ✅ §4c.1 | | `[[user]]` en la receta + validación + fusión idempotente | ✅ `crates/takana-core/src/user.rs`, §4d | | `takana users` (+`--merge`) y `targets.py --user-paths` | ✅ §4d | | `gitea` declara su servicio **y** su cuenta (hash sin mover) | ✅ `recipes/gitea.toml` | | El merge de cuentas en el camino de imagen | ✅ `scripts/servidor-image.sh` | | Que el resto de imágenes (escritorios) llamen al merge | ◻ hoy sólo `servidor-image.sh` | | `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 |