tawasuyu

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 1411Shell 141Markdown 85Rust 842918 Definiciones

servicios de paquete: la receta ya sabe declarar su demonio — y el card sale IGUAL al hardcodeado

Un paquete con servicio no tenía dónde decirlo: los dos del producto (hammerd,
sshd) vivían en constantes de Rust y los ocho de una sesión GNOME se lanzaban
con `&` desde un script, sin supervisión ni backoff ni el CRASHED real — o sea
sin nada de lo que arje es PID 1 para dar.

`[[service]]` en la receta, fuera de `hash_inputs` como license/slots/evidence:
declarar el servicio de openssh NO movió su hash, medido con el binario viejo
(que ignora el bloque) contra el nuevo, b3:938835e6… en los dos.

La prueba que autoriza el cambio no es que "parezca bien": el card generado
desde recipes/openssh.toml se compara ENTERO contra SSHD_SERVICE_CARD, que es
el que ya bootea en QEMU. Empatan ⇒ mover el servicio a la receta no cambiaría
un byte de la seed ni del product_rootfs_hash.

Por qué no alcanzaba `arje-absorb` (que existe y traduce systemd/openrc/runit/
dinit/sysvinit): sólo absorbe lo HABILITADO, los symlinks de <target>.wants/.
Un rootfs nuestro no tiene ese estado — medido: 62 .service sellados en el
store y CERO directorios .wants. Absorber devuelve vacío, y es la respuesta
correcta a la pregunta que absorb contesta. El enable de una distro construida
desde fuente no se lee del árbol: se declara.

a51d49a342ccbae8d8f4464bce62a000e0e829e8 · por Sergio · (hace 1 mes) · padre a8876c6d1 · ver el árbol en este commit

Delta semántico · 22

crates/takana-bootstrap/src/lib.rs

crates/takana-core/src/lib.rs

crates/takana-core/src/recipe.rs

crates/takana-core/src/service.rs

5 archivos · +393 −1

diff --git a/crates/takana-bootstrap/src/lib.rs b/crates/takana-bootstrap/src/lib.rs
index b3ee5e2d..98f6dca6 100644
--- a/crates/takana-bootstrap/src/lib.rs
+++ b/crates/takana-bootstrap/src/lib.rs
@@ -2083,6 +2083,31 @@ mod tests {
         assert!(sshd["supervision"]["Restart"]["initial"].is_number(), "sshd es Restart (lifecycle)");
     }
 
+    /// **La receta produce la MISMA Card que la constante hardcodeada.**
+    ///
+    /// Esta es la prueba que autoriza a mover el servicio de un literal de Rust a `[[service]]` en
+    /// `recipes/openssh.toml`: no compara «una card parecida», compara el `serde_json::Value`
+    /// entero contra [`SSHD_SERVICE_CARD`], que es **la card que el producto ya bootea en QEMU**
+    /// (verificada e2e por `scripts/ssh-e2e-test.sh`). Si empatan, declarar el servicio en la
+    /// receta no cambia ni un byte de la seed — y por lo tanto tampoco el `product_rootfs_hash`.
+    ///
+    /// Lee la receta REAL del repo a propósito: un fixture inline probaría el emisor contra sí
+    /// mismo y no detectaría que alguien editó la receta y la sacó de sincronía con el producto.
+    #[test]
+    fn la_receta_de_openssh_reproduce_la_card_hardcodeada() {
+        let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
+        let r = takana_core::Recipe::load_from_path(repo.join("recipes/openssh.toml")).unwrap();
+        assert_eq!(r.services.len(), 1, "openssh declara exactamente un servicio");
+
+        let generada = r.services[0].card();
+        let hardcodeada: serde_json::Value = serde_json::from_str(SSHD_SERVICE_CARD).unwrap();
+        assert_eq!(
+            generada, hardcodeada,
+            "la card generada desde recipes/openssh.toml difiere de SSHD_SERVICE_CARD: \
+             mover el servicio a la receta CAMBIARÍA la seed de producto"
+        );
+    }
+
     #[test]
     fn product_rootfs_hash_is_deterministic_and_sensitive() {
         let base = ArtifactHash::from_hex("ab");
diff --git a/crates/takana-core/src/lib.rs b/crates/takana-core/src/lib.rs
index 8372a9bf..9fcfed0d 100644
--- a/crates/takana-core/src/lib.rs
+++ b/crates/takana-core/src/lib.rs
@@ -17,6 +17,7 @@ pub mod proto;
 pub mod query;
 pub mod recipe;
 pub mod repo;
+pub mod service;
 pub mod sign;
 pub mod store;
 pub mod swm;
diff --git a/crates/takana-core/src/recipe.rs b/crates/takana-core/src/recipe.rs
index 83117bf7..507807f1 100644
--- a/crates/takana-core/src/recipe.rs
+++ b/crates/takana-core/src/recipe.rs
@@ -61,6 +61,21 @@ pub struct Recipe {
     /// build. Añadirla no mueve un solo `ArtifactHash`.
     #[serde(default, skip_serializing_if = "std::ops::Not::not")]
     pub foreign: bool,
+    /// **Los servicios que este paquete TRAE** (`[[service]]`), como Cards de arje.
+    ///
+    /// Es la respuesta al hueco que la distro tenía abierto: un paquete con demonio no tenía dónde
+    /// declararlo, así que los dos servicios del producto vivían hardcodeados en constantes de Rust
+    /// y los ocho de una sesión GNOME se lanzaban con `&` desde un script, sin supervisión. Ver
+    /// [`crate::service`] para el porqué completo (incluido por qué `arje-absorb` no lo cubre).
+    ///
+    /// Declara **qué es** el servicio, no **si arranca**: lo segundo es del PERFIL, igual que
+    /// systemd separa `[Service]` de `[Install]`.
+    ///
+    /// **NO entra en `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
+    /// solo `ArtifactHash`.
+    #[serde(default, rename = "service", skip_serializing_if = "Vec::is_empty")]
+    pub services: Vec<crate::service::Service>,
     /// Directorio base contra el que se resuelven rutas relativas de la receta
     /// (típicamente, `patches`). Lo fija `load_from_path`; al deserializar puro queda vacío.
     #[serde(skip, default)]
@@ -502,7 +517,13 @@ impl Recipe {
     /// Parsea una receta desde su forma TOML. `base_dir` queda vacío: si la receta declara
     /// `patches`, el caller debe fijar `base_dir` antes de hashear, o usar `load_from_path`.
     pub fn from_toml(s: &str) -> crate::Result<Recipe> {
-        toml::from_str(s).map_err(|e| crate::Error::Recipe(e.to_string()))
+        let r: Recipe = toml::from_str(s).map_err(|e| crate::Error::Recipe(e.to_string()))?;
+        // Los servicios se validan AL CARGAR, no al emitir la seed: un `id` mal puesto rompe la
+        // reproducibilidad del rootfs de producto, y eso hay que verlo en la receta —no tres pasos
+        // después, en un rootfs que ya no reproduce y no dice por qué. Recetas sin `[[service]]`
+        // pasan por una lista vacía, así que esto no toca a las 979 existentes.
+        crate::service::validate_all(&r.services)?;
+        Ok(r)
     }
 
     /// Serializa la receta a TOML. Útil para el sidecar de provenance que `takana-build`
diff --git a/crates/takana-core/src/service.rs b/crates/takana-core/src/service.rs
new file mode 100644
index 00000000..42252c6a
--- /dev/null
+++ b/crates/takana-core/src/service.rs
@@ -0,0 +1,323 @@
+//! `[[service]]` — **el servicio que un paquete TRAE**, traducido a una Card de arje.
+//!
+//! ## Por qué existe (el hueco, medido)
+//!
+//! Hasta hoy una receta no tenía dónde decir «yo traigo un demonio». La consecuencia no era
+//! teórica: los dos únicos servicios del producto (`hammerd` y `sshd`) vivían **hardcodeados en
+//! constantes de Rust** (`takana_bootstrap::SSHD_SERVICE_CARD`), y los ocho demonios de una sesión
+//! GNOME (dbus, logind-compat, accounts-daemon, upowerd, pipewire, pipewire-pulse, wireplumber,
+//! colord) se lanzaban a mano con `&` desde `scripts/gnome/gnome-start-qemu.sh` — **sin supervisión,
+//! sin backoff y sin el `CRASHED` real**, que es justo lo que arje sabe hacer y para lo que se lo
+//! adoptó como PID 1 (SDD 12).
+//!
+//! ## Por qué NO alcanza con `arje-absorb`
+//!
+//! arje ya trae el traductor de inits ajenos (`init/arje-absorb`, con lectores de systemd, OpenRC,
+//! runit, dinit y sysvinit). No sirve acá, y la razón es exacta: **`arje-absorb` sólo absorbe lo
+//! que está HABILITADO** — los symlinks de `<target>.wants/` — porque su trabajo es *retratar* un
+//! sistema vivo, no inventarle uno. Un rootfs de takana no tiene ese estado: medido el 2026-09-12
+//! sobre el store, hay **62 ficheros `.service` sellados (61 de usuario, 1 de sistema) y CERO
+//! directorios `.wants`**. Absorber un rootfs nuestro devuelve la lista vacía, y es la respuesta
+//! correcta a la pregunta que `arje-absorb` contesta.
+//!
+//! O sea que el «enable» de una distro construida desde fuente **no puede leerse del árbol: hay que
+//! declararlo**. Esa declaración se parte en dos, igual que systemd parte `[Service]` de
+//! `[Install]`:
+//!
+//! - **qué es el servicio** (exec, argv, supervisión) — propiedad del PAQUETE ⇒ este bloque;
+//! - **si arranca en esta imagen** — propiedad del PERFIL ⇒ `docs/state/targets.toml` (pendiente).
+//!
+//! ## Fuera de `hash_inputs`
+//!
+//! Igual que `license`, `slots` y `evidence`: describe **cómo se supervisa** el artefacto, no qué
+//! bytes tiene. `Recipe::hash_inputs` es una lista explícita de entradas, así que declarar servicios
+//! en recetas YA SELLADAS no mueve un solo `ArtifactHash` — que es lo único que hace la deuda
+//! pagable sin reconstruir el corpus.
+//!
+//! ## Límite conocido: no hay readiness
+//!
+//! arje ordena el arranque por **capacidades** (`resolve::plan_spawn`, orden topológico), pero eso
+//! ordena el *spawn*, no el *estar listo*: no existe primitiva «esperá a que aparezca este socket»
+//! (`DE-SYSTEMD-A-ARJE.md` §13 lo lista como hueco abierto — el `READY=1` de `Type=notify` se
+//! registra y no ordena). Mientras no exista, un servicio que necesita esperar a otro lo hace
+//! DENTRO de su propio card, con un `sh -c '…; exec …'` — que es exactamente lo que ya hace el card
+//! de `sshd` (levanta la red y genera las host keys antes del `exec`). Este módulo no lo esconde:
+//! lo deja explícito en `argv`.
+
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+
+/// Un servicio declarado por una receta. Se traduce 1:1 a una Card de arje ([`Service::card`]).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Service {
+    /// Nombre de la unidad (`sshd`). Es el `label` de la Card y la clave con la que `arjectl` la
+    /// nombra. Sin `/`: también es nombre de fichero en `cards.d/`.
+    pub label: String,
+    /// ULID de la Card. **Se declara, no se genera**: un id aleatorio haría que la seed —y con ella
+    /// el `product_rootfs_hash`— cambiara en cada corrida, rompiendo la reproducibilidad del
+    /// producto. Que sea estable es un requisito, no un detalle.
+    pub id: String,
+    /// El ELF a encarnar (`Payload::Native.exec`). Absoluto: lo ejecuta PID 1, que no tiene `PATH`.
+    pub exec: String,
+    #[serde(default)]
+    pub argv: Vec<String>,
+    /// Entorno explícito. arje inyecta además `ENTE_BUS_SOCK` y `ENTE_ID` por su cuenta.
+    #[serde(default)]
+    pub envp: Vec<(String, String)>,
+    /// `daemon` (default) u `oneshot` — el `lifecycle` de la Card.
+    #[serde(default)]
+    pub kind: Kind,
+    /// Política de supervisión. **Ausente ⇒ `OneShot`** (no revive). Presente ⇒ `Restart` con
+    /// backoff exponencial entre `initial_ms` y `max_ms`, y su muerte es el `CRASHED` real.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub restart: Option<Restart>,
+    /// Permiso de red del Ente. Default `none`: un demonio que no lo declara no toca la red.
+    #[serde(default)]
+    pub networking: Networking,
+    /// Slice de cgroup v2. Default `arje.slice/<label>`.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub cgroup: Option<String>,
+}
+
+/// `lifecycle` de la Card.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Kind {
+    #[default]
+    Daemon,
+    Oneshot,
+}
+
+impl Kind {
+    pub fn as_str(self) -> &'static str {
+        match self {
+            Kind::Daemon => "daemon",
+            Kind::Oneshot => "oneshot",
+        }
+    }
+}
+
+/// `permissions.networking` de la Card.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Networking {
+    #[default]
+    None,
+    Full,
+}
+
+impl Networking {
+    pub fn as_str(self) -> &'static str {
+        match self {
+            Networking::None => "none",
+            Networking::Full => "full",
+        }
+    }
+}
+
+/// Backoff exponencial ×2 desde `initial_ms`, saturado en `max_ms`; si el Ente sobrevive `max_ms`
+/// se considera estable y el backoff resetea (`sandokan-lifecycle`, fuente única de la política).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Restart {
+    pub initial_ms: u64,
+    pub max_ms: u64,
+}
+
+impl Service {
+    /// El slice de cgroup efectivo.
+    pub fn cgroup_path(&self) -> String {
+        self.cgroup.clone().unwrap_or_else(|| format!("arje.slice/{}", self.label))
+    }
+
+    /// La Card de arje, como `serde_json::Value`.
+    ///
+    /// La forma es la del esquema validado de `card_core::Card` (`schema_version` 1), verificada
+    /// contra `seeds/arje-qemu.card.json` y contra las dos cards que el producto ya bootea. Los
+    /// campos que este bloque no expone salen con el valor inerte que ya usaban esas cards
+    /// (`lineage: null`, `provides`/`requires` vacíos, namespaces todos `false`, rlimits `null`):
+    /// **no se inventan defaults nuevos**, se reproducen los que están en producción.
+    pub fn card(&self) -> Value {
+        let supervision = match &self.restart {
+            Some(r) => json!({ "Restart": { "initial": r.initial_ms, "max": r.max_ms } }),
+            None => json!("OneShot"),
+        };
+        json!({
+            "schema_version": 1,
+            "id": self.id,
+            "lineage": null,
+            "label": self.label,
+            "provides": [],
+            "requires": [],
+            "permissions": {
+                "networking": self.networking.as_str(),
+                "filesystem": "read-write",
+                "ipc": { "allow": [] },
+                "processes": true
+            },
+            "soma": {
+                "namespaces": {
+                    "mount": false, "pid": false, "net": false, "uts": false,
+                    "ipc": false, "user": false, "cgroup": false
+                },
+                "rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
+                "cgroup": { "path": self.cgroup_path(), "cpu_weight": null, "io_weight": null },
+                "cpu_affinity": null
+            },
+            "payload": { "Native": {
+                "exec": self.exec,
+                "argv": self.argv,
+                "envp": self.envp
+            }},
+            "supervision": supervision,
+            "lifecycle": self.kind.as_str(),
+            "priority": "normal",
+            "flow": { "input": [], "output": [] },
+            "genesis": []
+        })
+    }
+
+    /// Valida la FORMA (no la existencia del binario, que es cosa del artefacto).
+    pub fn validate(&self) -> crate::Result<()> {
+        let bad = |m: String| Err(crate::Error::Recipe(m));
+        if self.label.trim().is_empty() || self.label.contains('/') || self.label.contains("..") {
+            return bad(format!("service.label inválido: {:?} (sin `/`, no vacío)", self.label));
+        }
+        // El id es el ancla de la reproducibilidad de la seed: si falta o es basura, la Card cambia
+        // de identidad entre corridas y el `product_rootfs_hash` deja de ser estable.
+        if self.id.len() != 26 || !self.id.bytes().all(|b| b.is_ascii_alphanumeric()) {
+            return bad(format!(
+                "service[{}].id debe ser un ULID de 26 caracteres alfanuméricos, es {:?}",
+                self.label, self.id
+            ));
+        }
+        if !self.exec.starts_with('/') {
+            return bad(format!(
+                "service[{}].exec debe ser ruta absoluta (PID 1 no tiene PATH), es {:?}",
+                self.label, self.exec
+            ));
+        }
+        if let Some(r) = &self.restart {
+            if r.initial_ms == 0 || r.max_ms < r.initial_ms {
+                return bad(format!(
+                    "service[{}].restart: se espera 0 < initial_ms ≤ max_ms, es {}..{}",
+                    self.label, r.initial_ms, r.max_ms
+                ));
+            }
+        }
+        Ok(())
+    }
+}
+
+/// Valida una lista de servicios y rechaza labels e ids repetidos: dos Cards con el mismo `id` son
+/// la misma Card para el grafo de arje, y el segundo servicio se perdería en silencio.
+pub fn validate_all(services: &[Service]) -> crate::Result<()> {
+    let mut labels = std::collections::BTreeSet::new();
+    let mut ids = std::collections::BTreeSet::new();
+    for s in services {
+        s.validate()?;
+        if !labels.insert(&s.label) {
+            return Err(crate::Error::Recipe(format!("service.label duplicado: {:?}", s.label)));
+        }
+        if !ids.insert(&s.id) {
+            return Err(crate::Error::Recipe(format!(
+                "service.id duplicado: {:?} (lo usan {:?} y otro)",
+                s.id, s.label
+            )));
+        }
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn sshd() -> Service {
+        Service {
+            label: "sshd".into(),
+            id: "01HQAR53D4M2NBV8KZTYXFQA12".into(),
+            exec: "/bin/busybox".into(),
+            argv: ["sh", "-c", "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"]
+                .iter()
+                .map(|s| s.to_string())
+                .collect(),
+            envp: vec![("HOME".into(), "/root".into())],
+            kind: Kind::Daemon,
+            restart: Some(Restart { initial_ms: 500, max_ms: 20000 }),
+            networking: Networking::Full,
+            cgroup: Some("arje.slice/sshd".into()),
+        }
+    }
+
+    #[test]
+    fn card_tiene_la_forma_del_esquema() {
+        let c = sshd().card();
+        assert_eq!(c["label"], "sshd");
+        assert_eq!(c["payload"]["Native"]["exec"], "/bin/busybox");
+        assert_eq!(c["payload"]["Native"]["envp"][0][0], "HOME");
+        assert_eq!(c["supervision"]["Restart"]["initial"], 500);
+        assert_eq!(c["permissions"]["networking"], "full");
+        assert_eq!(c["soma"]["cgroup"]["path"], "arje.slice/sshd");
+        assert_eq!(c["lifecycle"], "daemon");
+    }
+
+    #[test]
+    fn sin_restart_es_oneshot() {
+        let mut s = sshd();
+        s.restart = None;
+        assert_eq!(s.card()["supervision"], json!("OneShot"));
+    }
+
+    #[test]
+    fn cgroup_por_defecto_deriva_del_label() {
+        let mut s = sshd();
+        s.cgroup = None;
+        assert_eq!(s.cgroup_path(), "arje.slice/sshd");
+    }
+
+    #[test]
+    fn se_deserializa_de_toml() {
+        let t = r#"
+label = "sshd"
+id    = "01HQAR53D4M2NBV8KZTYXFQA12"
+exec  = "/bin/busybox"
+argv  = ["sh", "-c", "exec /usr/sbin/sshd -D -e"]
+envp  = [["HOME", "/root"]]
+networking = "full"
+restart = { initial_ms = 500, max_ms = 20000 }
+"#;
+        let s: Service = toml::from_str(t).unwrap();
+        s.validate().unwrap();
+        assert_eq!(s.kind, Kind::Daemon); // default
+        assert_eq!(s.restart.unwrap().max_ms, 20000);
+    }
+
+    #[test]
+    fn validate_rechaza_lo_que_rompe_la_seed() {
+        let mut s = sshd();
+        s.id = "corto".into();
+        assert!(s.validate().unwrap_err().to_string().contains("ULID"));
+
+        let mut s = sshd();
+        s.exec = "sshd".into();
+        assert!(s.validate().unwrap_err().to_string().contains("absoluta"));
+
+        let mut s = sshd();
+        s.label = "a/b".into();
+        assert!(s.validate().unwrap_err().to_string().contains("label"));
+
+        let mut s = sshd();
+        s.restart = Some(Restart { initial_ms: 0, max_ms: 10 });
+        assert!(s.validate().unwrap_err().to_string().contains("initial_ms"));
+    }
+
+    #[test]
+    fn validate_all_rechaza_duplicados() {
+        let a = sshd();
+        let mut b = sshd();
+        b.label = "otro".into();
+        // mismo id, distinto label ⇒ colisión de identidad en el grafo
+        assert!(validate_all(&[a.clone(), b]).unwrap_err().to_string().contains("id duplicado"));
+        assert!(validate_all(&[a.clone(), a]).unwrap_err().to_string().contains("label duplicado"));
+    }
+}
diff --git a/recipes/openssh.toml b/recipes/openssh.toml
index d0d2dd58..0ac6629a 100644
--- a/recipes/openssh.toml
+++ b/recipes/openssh.toml
@@ -77,3 +77,25 @@ build = ["zlib", "openssl", "perl"]
 configure = "./configure --prefix=/usr --sysconfdir=/etc/ssh --with-ssl-dir=/usr --with-zlib=/usr --with-privsep-path=/var/empty --without-pam --without-selinux --without-hardening CC='zig cc -mcpu=baseline -target x86_64-linux-musl' LDFLAGS=-static && sed -i '/^CHANNELLIBS=/{s/-lcrypto/-l:libcrypto.a/g;s/-lz/-l:libz.a/g}' Makefile"
 compile   = "make -j\"$(nproc)\""
 install   = "make install-nokeys DESTDIR=/out STRIP_OPT="
+
+# ── EL SERVICIO QUE ESTE PAQUETE TRAE ───────────────────────────────────────────────────────────
+# Hasta hoy esta Card vivía HARDCODEADA en `takana_bootstrap::SSHD_SERVICE_CARD`, una constante de
+# Rust: el paquete que trae el demonio no tenía dónde decirlo, así que lo decía el binario de takana.
+# El `id` es el ULID que esa constante ya usaba — **cambiarlo movería la seed de producto y con ella
+# el `product_rootfs_hash`**, así que se preserva tal cual, no se regenera.
+#
+# El `argv` hace tres cosas antes del `exec` porque arje NO TIENE readiness por unidad (ver
+# `takana_core::service`): levanta la red, genera las host keys al primer boot, y recién ahí encarna
+# sshd. Eso no es una maña de esta receta — es el patrón obligado mientras el hueco siga abierto, y
+# por eso está a la vista en el argv en vez de escondido en un wrapper.
+#
+# Fuera de `hash_inputs`: declarar esto NO re-hashea openssh.
+[[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 }