El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta. Los USUARIOS se quedaron
donde estaban las Cards: `/etc/passwd` de la imagen es `takana_bootstrap::PRODUCT_PASSWD`, un literal
con `root` y `sshd`, y añadir un tercero era editar Rust y recompilar takana.
No es simetría por elegancia, es un servicio que no arranca: `gitea` se niega a correr como root, su
Card hace `setuidgid gitea`, y sin la cuenta arranca, muere y reintenta para siempre con un log que
dice `unknown user` — no «a la imagen le falta una cuenta».
[[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`, medido: el hash de gitea no se movió (`b3:391a613e…` antes y después).
`takana users <recetas…> [--merge <rootfs>]` es el gemelo de `service-cards`, y fusiona **por clave,
no por línea entera** — componer dos veces no duplica, y `grep -q` de la línea completa no serviría
porque la misma cuenta con otro GECOS se leería como nueva. Tres decisiones con su control:
· Una cuenta ya presente con OTRA línea es 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.
· Se planea todo y sólo entonces se escribe. Fichero a fichero, un conflicto en `passwd` dejaba el
`group` ya escrito: media cuenta es peor que ninguna, porque parece que está. Comprobado con el
caso exacto — `group` sin la cuenta, `passwd` con otro uid — y los DOS ficheros quedan intactos.
· Un `passwd` ausente es un error, no un fichero a crear: crearlo dejaría una imagen SIN `root`.
La validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids, uid fuera de
100..=65533, `home` relativo y un `:` en cualquier campo — que partiría la línea y fallaría lejos.
Y `--user-paths` NO es `--service-paths` con otro nombre: aquél lista los servicios HABILITADOS,
éste los paquetes INSTALADOS que declaran cuentas. `postgres` instalado y sin levantar necesita su
usuario igual, porque los ficheros de la imagen ya son suyos.
`scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen, y aborta si hay
conflicto: un rootfs con el uid equivocado produce ficheros de un dueño que no existe.
Verde: 6 tests del módulo, core 234, cli 88, bootstrap 42, `targets.py --selftest` 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
401 lines
25 KiB
Markdown
401 lines
25 KiB
Markdown
# 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 `<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 — 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 — 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 <recetas…>`** — 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 <perfil>`** — 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 <recetas…> --merge <rootfs>` 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 |
|