Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

crates/skidbladnir-systemd/src/service.rs

//! INSTALL-TREE — the backend-neutral service-install abstraction.
//!
//! `src/systemd.rs` is the *first* install backend: a `SystemdServiceSpec` that
//! renders straight to a systemd `.service` unit (system + `--user` scope). That
//! is excellent for systemd hosts, but a 2015+ install tree must also target
//! **OpenRC · runit · s6-rc · dinit · SysV** (and OCI/quadlet/Windows for
//! completeness). Hard-coupling every product to systemd would re-create the
//! per-init bespoke install scripts Skíðblaðnir set out to delete.
//!
//! This module introduces the **tree**: ONE declarative [`ServiceSpec`](crate::service::ServiceSpec) (exec,
//! user, scope, env, deps, restart, sockets) that renders to ALL of them through
//! a [`ServiceRenderer`](crate::service::ServiceRenderer) trait — one renderer per init/service backend. The spike
//! lands **two real renderers**:
//!
//! 1. [`SystemdRenderer`](crate::service::SystemdRenderer) — bridges to the existing, tested
//!    [`crate::systemd::systemd_unit_scoped`] (system + user scope), so the tree
//!    *reuses* the install limb that already ships, it does not fork it.
//! 2. [`OpenRcRenderer`](crate::service::OpenRcRenderer) — an `#!/sbin/openrc-run` service script + a
//!    `/etc/conf.d/<name>` env file. A deliberately *non-systemd* second backend:
//!    it proves the abstraction is init-neutral, not just "systemd with a coat of
//!    paint".
//!
//! Everything here is **pure** (no root, no IO, no subprocess): a renderer turns a
//! [`ServiceSpec`](crate::service::ServiceSpec) into a [`RenderedService`](crate::service::RenderedService) — the file bodies to write plus the
//! enable/start/status/stop **verbs as data** (argv vectors). A thin privileged
//! `apply()` layer (writing files + invoking the verbs) is a follow-up; keeping
//! render pure is what makes the whole tree unit-testable with zero privilege,
//! exactly like the systemd generators.
//!
//! Design + the full multi-init landscape survey + the KVM test-matrix/probe plan:
//! `.nornir/install-tree-design.md`.

use serde::{Deserialize, Serialize};

// The tree is a *generalization* of the systemd limb, so it reuses that limb's
// vocabulary rather than cloning it: scope (system vs per-user) and the restart
// policy mean the same thing in every backend.
pub use crate::systemd::{
    MemoryBound, MemoryBoundRefused, OomPolicy, OomPolicyRefused, OomScoreAdjust,
    OomScoreAdjustRefused, RestartPolicy, Scope, StartLimit, StartLimitRefused,
};

/// The init / service-manager backends a 2015+ install tree targets. Rendered
/// today: [`Backend::SystemdSystem`], [`Backend::SystemdUser`], [`Backend::OpenRc`],
/// [`Backend::SysV`], [`Backend::Runit`], [`Backend::S6Rc`], [`Backend::Dinit`],
/// [`Backend::Kubernetes`], [`Backend::Oci`], [`Backend::Quadlet`] and
/// [`Backend::WindowsService`] — **all eleven** now render. [`Backend::all`] drives a
/// full-coverage matrix; every backend resolves to `Some` in [`renderer_for`] (no
/// honest `None` remains).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Backend {
    /// systemd, system scope — root unit in `/etc/systemd/system`, runs as a
    /// dedicated system user. **Implemented** (bridges [`crate::systemd`]).
    SystemdSystem,
    /// systemd, `--user` scope — unit in `~/.config/systemd/user`, runs as the
    /// caller. **Implemented** (bridges [`crate::systemd`], `Scope::User`).
    SystemdUser,
    /// OpenRC — `#!/sbin/openrc-run` script in `/etc/init.d` + `/etc/conf.d`
    /// env. **Implemented**.
    OpenRc,
    /// runit — a `run` (+ optional `finish`/`log/run`) executable service dir
    /// under `/etc/sv/<name>`, symlinked into `/var/service`. **Implemented**.
    Runit,
    /// s6-rc — a compiled source-definition dir (`type`, `run`, `finish`,
    /// `dependencies.d/`, `env/`) driven by `s6-rc`. **Implemented**.
    S6Rc,
    /// dinit — a textual `key = value` service description in `/etc/dinit.d/<name>`
    /// (+ an `env-file`), driven by `dinitctl`. **Implemented**.
    Dinit,
    /// SysV init — an LSB `/etc/init.d/<name>` script with an init-info header,
    /// driving the daemon via `start-stop-daemon` + an `/etc/default/<name>` env
    /// file. The universal fallback. **Implemented**.
    SysV,
    /// OCI / Docker — a `podman`-style container the runtime supervises (the
    /// `--restart` policy stands in for unit supervision *and* enable-at-boot; the
    /// whole spec is the `podman run` argv + a 0600 `--env-file`). **Implemented**.
    Oci,
    /// Podman **quadlet** — a systemd `.container` unit `podman-systemd` (the
    /// `quadlet` generator) projects into a `<name>.service` on `daemon-reload`.
    /// **Implemented** (reuses the OCI image-ref + the systemd `[Install]`/`[Unit]`
    /// conventions).
    Quadlet,
    /// Windows service (SCM) — a WiX `.wxs` service fragment (reusing the MSI
    /// `crate::msi::WindowsServiceSpec` seam) + `sc.exe` create/start/query/stop
    /// verbs. **Implemented**.
    WindowsService,
    /// Kubernetes — a Deployment/StatefulSet + Service (+ PVC) rendered as
    /// manifest data. **Implemented** (sync renderer, [`crate::k8s`]); the async
    /// applier that drives the manifests over the k8s API lands behind the
    /// `kubernetes` feature.
    Kubernetes,
}

impl Backend {
    /// Every backend in the tree (drives a render-coverage matrix: which backends
    /// a given [`ServiceSpec`] can be expressed on today).
    pub fn all() -> &'static [Backend] {
        use Backend::*;
        &[
            SystemdSystem,
            SystemdUser,
            OpenRc,
            Runit,
            S6Rc,
            Dinit,
            SysV,
            Oci,
            Quadlet,
            WindowsService,
            Kubernetes,
        ]
    }

    /// Stable slug (filenames, telemetry, the test matrix' backend axis).
    pub fn slug(self) -> &'static str {
        use Backend::*;
        match self {
            SystemdSystem => "systemd-system",
            SystemdUser => "systemd-user",
            OpenRc => "openrc",
            Runit => "runit",
            S6Rc => "s6-rc",
            Dinit => "dinit",
            SysV => "sysv",
            Oci => "oci",
            Quadlet => "quadlet",
            WindowsService => "windows-service",
            Kubernetes => "kubernetes",
        }
    }
}

/// A socket the service listens on — the backend-neutral form of systemd socket
/// activation. Carried in the spec so a renderer that supports it (systemd `.socket`)
/// can wire it, and one that does not (OpenRC) records it as a documented port the
/// service binds itself. `port` is the TCP port; `stream` distinguishes
/// stream(TCP)/dgram(UDP) for the future `.socket` render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Socket {
    pub port: u16,
    pub stream: bool,
}

impl Socket {
    pub fn tcp(port: u16) -> Self {
        Socket { port, stream: true }
    }
}

/// Fat vs thin agent — the constellation-wide split nornir already ships as its
/// server + thin-client (`--server` / `NORNIR_SERVER`). A [`AgentMode::Fat`] node
/// is the full process that holds the warehouse/state and answers RPC; a
/// [`AgentMode::Thin`] node is a client that routes to a fat peer instead of
/// holding state locally. Carried on the [`ServiceSpec`] so holger and dwarfs
/// inherit the exact same fat/thin lifecycle nornir has, for free, once they
/// declare a spec. The mode is service metadata — it classifies the node; the
/// concrete wiring (which peer a thin node routes to) is an env/arg on the spec.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AgentMode {
    /// The full server process: holds the warehouse/state, answers RPC. Default.
    #[default]
    Fat,
    /// A client that routes to a fat peer (nornir's `--server`/`NORNIR_SERVER`
    /// pattern) rather than holding state locally.
    Thin,
}

/// How the [`Lifecycle`] verifies a started service is actually *up* (beyond
/// `systemctl is-active`). The backend-neutral form of the AIRGAP6
/// [`crate::product::HealthCheck`]; carried on the [`ServiceSpec`] so `health()`
/// can probe without re-declaring it per product.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum HealthProbe {
    /// No active probe — the unit being `active` is health enough.
    #[default]
    None,
    /// TCP connect to `127.0.0.1:<port>` must succeed. Loopback by definition —
    /// use [`HealthProbe::TcpAt`] for a service that binds anything else.
    Tcp(u16),
    /// TCP connect to `<host>:<port>` must succeed — the **host-carrying** form of
    /// [`HealthProbe::Tcp`], for a service that does NOT bind loopback.
    ///
    /// A probe that only ever knocks on `127.0.0.1` marks a perfectly healthy
    /// service DEAD the moment it is moved off loopback: korp-server on oden was
    /// re-bound to its tailscale address `100.115.167.95:50051`, served fine, and
    /// still failed every `--refresh` health check (and rolled the generation back)
    /// because the probe dialled a loopback port nothing listened on. Carry the
    /// address the service actually binds and the probe asks the right socket.
    ///
    /// A **wildcard** host (`0.0.0.0`, `::`, `[::]`, `*`, or empty) is not a dialable
    /// peer: it means "every interface", so the probe falls back to loopback
    /// (`127.0.0.1`, then `::1`) — see [`ServiceSpec::health`] / [`Lifecycle::health`].
    ///
    /// Build it with [`HealthProbe::tcp_at`] or, from a bind string the product
    /// already holds, [`HealthProbe::tcp_addr`].
    TcpAt {
        /// The host/IP the service binds (NOT necessarily loopback).
        host: String,
        /// The port the service binds.
        port: u16,
    },
    /// HTTP `GET <path>` on the service's first listening port must return 2xx.
    ///
    /// Dials **loopback** by definition, exactly like [`HealthProbe::Tcp`]. For a service
    /// bound off-loopback use [`HealthProbe::HttpAt`].
    Http(String),
    /// HTTP `GET <path>` against the host the service actually BINDS, on its first
    /// listening port.
    ///
    /// The HTTP twin of [`HealthProbe::TcpAt`], and it exists for the same measured reason:
    /// [`http_probe`] hardcoded `Ipv4Addr::LOCALHOST`, so an HTTP-probed service bound to a
    /// tailscale/LAN address could never pass a health check and every refresh of it rolled
    /// back. `TcpAt` was added first (korp-server hit it on oden 2026-08-11); this is the
    /// same defect on the other probe, fixed before a second product finds it the hard way.
    ///
    /// A wildcard host (`0.0.0.0`, `[::]`, `*`, empty) probes loopback — connecting to
    /// `0.0.0.0` is meaningless and such a service is reachable on `127.0.0.1`.
    HttpAt {
        /// The host the service binds — bracketed or bare IPv6 both accepted.
        host: String,
        /// The path to `GET`; a missing leading `/` is added.
        path: String,
    },
    /// **Connect, read what the server SAYS FIRST, and require `expect` in it.**
    ///
    /// For the large family of line-protocol servers that greet an unprompted client
    /// — SSH (`SSH-2.0-…`, RFC 4253 §4.2), SMTP (`220 …`), FTP, IMAP, POP3 — the
    /// greeting *is* the application answering, and it costs one connect to get.
    ///
    /// # Why it is not just [`HealthProbe::Tcp`]
    ///
    /// A TCP connect proves a socket is bound. It cannot tell the intended server
    /// from anything else that grabbed the port, from a process wedged after `bind`
    /// but before it can speak, or from an SSH tunnel forwarding to a dead peer. This
    /// arm reads bytes off the wire and requires the server's own protocol identifier
    /// in them.
    ///
    /// MEASURED here 2026-08-15 against `gunnar-server serve --ssh-bind
    /// 127.0.0.1:22222`: `SSH-2.0-gunnar_1.1.3 v1.1.3-58-g99f8de0-modified\r\n` — which
    /// names the product AND the build, so `expect: "SSH-2.0-gunnar"` is a claim about
    /// *what* answered, not merely *that* something did.
    ///
    /// # What it does NOT prove
    ///
    /// A greeting is the first thing a server writes; it is not a transaction. It does
    /// not prove the service can authenticate, read its data, or serve a repository —
    /// only that the process on that port is the right program and got far enough to
    /// speak. For a protocol with no unprompted greeting (HTTP, gRPC) this arm hangs
    /// until its 2 s read timeout and reports unhealthy; use
    /// [`HealthProbe::Http`]/[`HealthProbe::HttpAt`] there.
    Banner {
        /// The host the service binds. Wildcards fold to loopback, exactly as in
        /// [`HealthProbe::TcpAt`].
        host: String,
        /// The port the service binds.
        port: u16,
        /// A substring the greeting must contain (`"SSH-2.0-gunnar"`, `"220 "`, …).
        expect: String,
    },
}

impl HealthProbe {
    /// A [`HealthProbe::TcpAt`] from a host and a port — the host-carrying TCP probe.
    ///
    /// ```
    /// use skidbladnir_systemd::service::HealthProbe;
    /// let p = HealthProbe::tcp_at("100.115.167.95", 50051);
    /// assert_eq!(p, HealthProbe::TcpAt { host: "100.115.167.95".into(), port: 50051 });
    /// ```
    pub fn tcp_at(host: impl Into<String>, port: u16) -> Self {
        HealthProbe::TcpAt {
            host: host.into(),
            port,
        }
    }

    /// A [`HealthProbe::TcpAt`] from the ONE bind string a product already carries
    /// (`KORP_SERVER_ADDR`, `--listen`, …) — `host:port`, or `[v6]:port`.
    ///
    /// `None` when there is no parseable port, so a caller never turns a typo into a
    /// silently loopback (or portless) probe; see [`split_bind_addr`].
    ///
    /// ```
    /// use skidbladnir_systemd::service::HealthProbe;
    /// assert_eq!(
    ///     HealthProbe::tcp_addr("100.115.167.95:50051"),
    ///     Some(HealthProbe::tcp_at("100.115.167.95", 50051))
    /// );
    /// assert_eq!(HealthProbe::tcp_addr("garbage"), None);
    /// ```
    pub fn tcp_addr(addr: &str) -> Option<Self> {
        split_bind_addr(addr).map(|(host, port)| HealthProbe::tcp_at(host, port))
    }

    /// A [`HealthProbe::Banner`] from a host, a port and the greeting substring.
    ///
    /// ```
    /// use skidbladnir_systemd::service::HealthProbe;
    /// let p = HealthProbe::banner("127.0.0.1", 22222, "SSH-2.0-gunnar");
    /// assert_eq!(p, HealthProbe::Banner {
    ///     host: "127.0.0.1".into(), port: 22222, expect: "SSH-2.0-gunnar".into(),
    /// });
    /// ```
    pub fn banner(host: impl Into<String>, port: u16, expect: impl Into<String>) -> Self {
        HealthProbe::Banner {
            host: host.into(),
            port,
            expect: expect.into(),
        }
    }

    /// A [`HealthProbe::Banner`] from the ONE bind string a product already carries
    /// plus the greeting substring. `None` when there is no parseable port — the same
    /// refusal [`tcp_addr`](HealthProbe::tcp_addr) makes, for the same reason.
    ///
    /// ```
    /// use skidbladnir_systemd::service::HealthProbe;
    /// assert_eq!(
    ///     HealthProbe::banner_addr("127.0.0.1:22222", "SSH-2.0-"),
    ///     Some(HealthProbe::banner("127.0.0.1", 22222, "SSH-2.0-"))
    /// );
    /// assert_eq!(HealthProbe::banner_addr("garbage", "SSH-2.0-"), None);
    /// ```
    pub fn banner_addr(addr: &str, expect: impl Into<String>) -> Option<Self> {
        split_bind_addr(addr).map(|(host, port)| HealthProbe::banner(host, port, expect))
    }
}

/// Split a bind string into `(host, port)` — `127.0.0.1:50051`, `100.115.167.95:50051`,
/// `[::1]:50051` (brackets kept, the probe strips them), `[::]:50051`.
///
/// The ONE parser behind [`HealthProbe::tcp_addr`] and
/// [`crate::product::HealthCheck::tcp_addr`], so a product never hand-rolls the
/// IPv6-bracket split itself. `None` when the last colon-separated field is not a
/// port (an honest `None` beats a defaulted port that probes the wrong socket).
pub fn split_bind_addr(addr: &str) -> Option<(&str, u16)> {
    let addr = addr.trim();
    let (host, port) = addr.rsplit_once(':')?;
    // `[::1]:50051` splits cleanly; a BARE `::1` (no port) must not be read as
    // host `::` + port `1`, so reject a host that still ends in a colon.
    if host.ends_with(':') && !host.ends_with(']') {
        return None;
    }
    let port: u16 = port.parse().ok()?;
    Some((host, port))
}

/// A configuration file a service reads, carried in its [`ServiceSpec`]
/// ([`ServiceSpec::config_files`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigFile {
    /// Absolute path the service reads it at (on the host, or inside the container).
    pub path: String,
    /// The file's UTF-8 contents.
    pub contents: String,
    /// Permission bits; [`ConfigFile::DEFAULT_MODE`] unless set.
    #[serde(default = "ConfigFile::default_mode")]
    pub mode: u32,
}

impl ConfigFile {
    pub const DEFAULT_MODE: u32 = 0o644;
    fn default_mode() -> u32 {
        Self::DEFAULT_MODE
    }

    /// Every path absolute, normalised, control-free and declared once; every mode
    /// within 0777.
    pub(crate) fn check_all(files: &[ConfigFile]) -> Result<(), String> {
        let mut seen = std::collections::BTreeSet::new();
        for f in files {
            crate::systemd::check_no_control("config file path", &f.path)?;
            let normal = f.path.starts_with('/')
                && f.path[1..].split('/').all(|c| !matches!(c, "" | "." | ".."));
            if !normal {
                return Err(format!(
                    "config file path `{}` must be absolute and normalised (no empty, `.` or `..` component)",
                    f.path
                ));
            }
            if f.mode > 0o777 {
                return Err(format!("config file `{}`: mode {:o} > 0777", f.path, f.mode));
            }
            if !seen.insert(f.path.as_str()) {
                return Err(format!("config file `{}` declared twice", f.path));
            }
        }
        Ok(())
    }
}

/// The host-backend projection of [`ServiceSpec::config_files`]: one
/// [`RenderedFile`] per declared file, at its own path and mode.
fn config_rendered_files(spec: &ServiceSpec) -> impl Iterator<Item = RenderedFile> + '_ {
    spec.config_files.iter().map(|f| RenderedFile {
        path: f.path.clone(),
        contents: f.contents.clone(),
        mode: f.mode,
    })
}

/// `<path>:<path>:ro` bind mounts for a container backend, one per config file.
/// `:` splits the mount spec and whitespace/quotes split the quadlet line, so a
/// path carrying any of them is refused rather than mounted somewhere else.
fn container_config_mounts(spec: &ServiceSpec, backend: &str) -> anyhow::Result<Vec<String>> {
    spec.config_files
        .iter()
        .map(|f| {
            if f.path.chars().any(|c| c == ':' || c == ',' || c == '"' || c == '\'' || c == '\\' || c.is_whitespace()) {
                anyhow::bail!(
                    "{backend}: config file path `{}` cannot be a bind mount (contains `:`, `,`, a quote, a backslash or whitespace)",
                    f.path.escape_debug()
                );
            }
            Ok(format!("{0}:{0}:ro", f.path))
        })
        .collect()
}

/// The ONE declarative service spec at the top of the install tree. Backend-neutral
/// superset of [`crate::systemd::SystemdServiceSpec`] and
/// [`crate::product::ComponentSpec`]; a [`ServiceRenderer`] projects it onto a
/// concrete init system. `serde`, so a product embeds it in its airgap bundle.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceSpec {
    /// Service name without any suffix (e.g. `"holger"`).
    pub name: String,
    /// Human description (unit `Description=`, OpenRC `description=`).
    pub description: String,
    /// Absolute path of the installed binary to exec.
    pub exec: String,
    /// Args appended to `exec` (space-joined for the rendered command line).
    pub args: Vec<String>,
    /// Run-as user. `None` ⇒ run as the caller (the only valid choice for
    /// [`Scope::User`]); a renderer omits the user directive in that case.
    pub user: Option<String>,
    /// Run-as group (`Group=` / OpenRC `command_group`). `None` ⇒ the renderer
    /// defaults it to `user` (systemd's `Group=` mirrors `User=`).
    #[serde(default)]
    pub group: Option<String>,
    /// **ADDITIONAL groups the service process joins** — systemd
    /// `SupplementaryGroups=`. See [`ServiceSpec::supplementary_group`].
    ///
    /// Distinct from [`group`](ServiceSpec::group), which is the single PRIMARY
    /// group (`Group=`): a service needs its own primary group *and* membership of
    /// somebody else's group to open somebody else's file. There was no way to say
    /// the second, so the only route was a hand-written drop-in that no install
    /// writes and no uninstall removes.
    ///
    /// # Which backends honour it
    ///
    /// **Honoured, with a real native equivalent:**
    ///
    /// * **systemd** — `SupplementaryGroups=`, one space-separated directive. System
    ///   scope only: a `systemctl --user` unit runs as the caller with the caller's
    ///   groups already applied, and systemd rejects the directive there exactly as
    ///   it rejects `User=`.
    /// * **OCI** — `--group-add <g>`, one flag per group, on the `podman run` argv.
    /// * **Quadlet** — `GroupAdd=<g>` in `[Container]`, which is the key that becomes
    ///   podman's `--group-add`. NOT the unit's `SupplementaryGroups=`: the generated
    ///   `.service` describes a container, so the groups must land on the container
    ///   process, not on the supervising unit.
    ///
    /// **NOT honoured — named in the output rather than dropped in silence:**
    ///
    /// * **OpenRC** — `command_user` is passed to `start-stop-daemon --user`, which
    ///   takes one `user[:group]` pair. The script carries a `NOT HONOURED` comment
    ///   naming the groups it could not apply.
    ///
    /// **Not honoured and currently SILENT — a known residue, not a design:** sysvinit
    /// (`start-stop-daemon --chuid`), runit (`chpst -u`), dinit (`run-as =`), s6-rc
    /// (`s6-setuidgid`) and the Windows service renderer. Some of these have a partial
    /// route (`chpst -u user:g1:g2` takes supplementary groups; `s6-setuidgid` already
    /// applies whatever the host's group database gives the account), so the correct
    /// fix differs per backend and is deliberately not guessed here. Declare against
    /// systemd/OCI/Quadlet and the directive is real; declare against those five and
    /// it is currently lost.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub supplementary_groups: Vec<String>,
    /// **THE RESOURCE CEILING — how much memory this service may hold before the
    /// system does something about it.** `None` ⇒ no ceiling is declared and no
    /// backend emits anything, so every already-rendered unit/script is byte-identical.
    ///
    /// Two DIFFERENT facts, carried on one value and never collapsed:
    /// a **soft** ceiling (`MemoryHigh=`, the service gets SLOW — throttled and
    /// reclaimed) and a **hard** ceiling (`MemoryMax=`, THIS cgroup is OOM-killed and
    /// [`ServiceSpec::restart`] brings it back). Stored in BYTES; each backend's
    /// spelling is derived from the number. Set it with [`ServiceSpec::memory`] —
    /// which REFUSES, by name and naming both numbers, a hard ceiling below the soft
    /// one, a zero ceiling, and a bound declaring neither.
    ///
    /// # Why the axis exists — dwarf, 2026-08-20
    ///
    /// `dwarves-server` reached **33 GB RSS**. Its unit declared no memory bound of
    /// any kind, so the kernel OOM killer chose freely across the whole 62 GB box: it
    /// took `dwarves-server` **and** `gunnar-service`, and the machine went down. With
    /// `Restart=on-failure` already in the unit, the failure mode of a *bounded*
    /// service would have been a two-second blip. Unbounded, the failure mode was not
    /// "the service dies" but **"the box dies"**.
    ///
    /// Skidbladnir had NO resource axis at all —
    /// `grep -rniE 'memory(high|max|limit)|CPUQuota' src/` returned zero — so the only
    /// route was a hand-written
    /// `/etc/systemd/system/dwarves-server.service.d/10-bounded.conf` carrying
    /// `MemoryHigh=16G` / `MemoryMax=24G`. That file is exactly what dwarves' own law
    /// forbids: *"NEVER hand-write a `<unit>.service.d/*.conf`: a drop-in is owned by
    /// nobody, survives uninstall and re-applies to the next install."* The law was
    /// broken because the declaration had nowhere to live. Declared here, the bound is
    /// written by the install and removed by the uninstall with the fragment itself.
    ///
    /// **LANDED — read back off the live manager on oden, 2026-08-26.** The drop-in is
    /// GONE: `systemctl show dwarves-server` reports an EMPTY `DropInPaths=`, and the
    /// one rendered fragment `/etc/systemd/system/dwarves-server.service` carries
    /// `MemoryHigh=24G` / `MemoryMax=28G` / `OOMPolicy=continue` /
    /// `OOMScoreAdjust=500` — the manager resolves those to 25769803776 and
    /// 30064771072 bytes. Those are the values the service RUNS at today, declared
    /// through this axis by the consumer; the `16G`/`24G` above is the drop-in's
    /// original 2026-08-20 pair, kept because it is what the incident produced.
    /// Recorded here so the axis is judged on APPLIED OUTPUT — a directive a live
    /// manager parsed and reports back — rather than on a rendered string alone.
    ///
    /// # Which backends honour it
    ///
    /// **Honoured, with a real native equivalent:**
    ///
    /// * **systemd — system AND user scope** — `MemoryHigh=` / `MemoryMax=`. Unlike
    ///   `User=` and `SupplementaryGroups=`, this is valid in a `systemctl --user`
    ///   unit: user units get their own cgroup under `user@<uid>.service` and systemd
    ///   delegates the memory controller into it. MEASURED on dwarf (systemd 259,
    ///   2026-08-24): `DelegateControllers=cpu memory pids`, and a transient
    ///   `systemd-run --user -p MemoryMax=64M -p MemoryHigh=32M` unit read back
    ///   `memory.max` 67108864 / `memory.high` 33554432 from its live cgroup. So the
    ///   directives are deliberately NOT inside the renderer's `!scope.is_user()`
    ///   block — putting them there by reflex would drop a bound a user service can
    ///   genuinely have.
    /// * **OCI (podman)** — `--memory=<hard>` (podman writes `memory.max`, the same
    ///   file `MemoryMax=` writes) and `--memory-reservation=<soft>`. The hard ceiling
    ///   is EXACT. The soft one is the nearest thing podman exposes and is *near*, not
    ///   equal: `--memory-reservation` lands on cgroup-v2 `memory.low` (protection
    ///   from reclaim) rather than `memory.high` (throttle under reclaim). podman has
    ///   no `--memory-high`, so the alternative was to drop the soft ceiling
    ///   silently; naming the difference here and rendering the nearest real knob is
    ///   the honest trade.
    /// * **Quadlet** — the native `[Container]` key `Memory=<hard>` (which becomes
    ///   podman's `--memory`), plus `PodmanArgs=--memory-reservation=<soft>` because
    ///   quadlet exposes no native soft key. NOT the unit's `MemoryMax=`: the
    ///   generated `.service` describes a container, and the ceiling belongs on the
    ///   container — the same wrong-side-of-the-seam trap as
    ///   `SupplementaryGroups=` vs `GroupAdd=`.
    /// * **Kubernetes** — `resources.limits.memory` (hard) and
    ///   `resources.requests.memory` (soft). The limit is exact (the kubelet's
    ///   `memory.max`); `requests` is a SCHEDULING guarantee rather than a throttle,
    ///   which is the same near-miss as podman's reservation and is stated here for
    ///   the same reason.
    /// * **OpenRC** — `rc_cgroup_settings` (`memory.high` / `memory.max`), OpenRC's
    ///   own cgroup-v2 route, plus the `rc_cgroup_mode="unified"` it requires. The
    ///   rendered script ALSO carries a `# skidbladnir:` note saying so, because on a
    ///   host not running cgroups v2 unified OpenRC ignores the settings and the
    ///   bound would be a comment pretending to be a ceiling. Not verified against a
    ///   live OpenRC host from here (dwarf is systemd) — the precondition is named in
    ///   the output rather than assumed away.
    ///
    /// **NOT honoured — named in the output rather than dropped in silence:**
    ///
    /// * **runit**, **s6-rc**, **dinit**, **SysV**, **WindowsService**. Each renders a
    ///   `NOT HONOURED` note quoting both numbers, so an operator reading the artifact
    ///   knows precisely what to grant by hand.
    ///
    ///   All five *look* like they have a route and do not. `chpst -m` (runit),
    ///   `s6-softlimit -m` (s6), `rlimit-data =` (dinit) and `ulimit -v` (SysV) set
    ///   **POSIX rlimits**, which are a different mechanism with a different failure
    ///   mode: an rlimit caps one process's *address space* (and is inherited
    ///   per-process, not accounted per-service), it does not reclaim, it has no soft
    ///   ceiling at all, and when it bites the allocation fails inside the program —
    ///   `malloc` returns NULL, the service misbehaves in its own way — instead of the
    ///   cgroup being killed cleanly and restarted. Rendering an rlimit here would be
    ///   claiming to have bounded the service's memory while leaving the box exactly
    ///   as exposed as it was on 2026-08-20. Windows' nearest equivalent, a job
    ///   object's `JOB_OBJECT_LIMIT_JOB_MEMORY`, is real but reachable from neither
    ///   `sc.exe` nor a WiX `<ServiceInstall>`.
    ///
    /// **Silent:** none. Every one of the eleven backends either renders a real
    /// ceiling or says in its own output that it could not.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub memory: Option<MemoryBound>,
    /// **THE REACTION — what happens to the SERVICE when the ceiling above is
    /// actually hit.** `None` ⇒ no reaction is declared and no backend emits
    /// anything, so every already-rendered unit/script is byte-identical.
    ///
    /// [`ServiceSpec::memory`] declares WHEN a process of this service is reaped.
    /// This declares WHAT THEN HAPPENS TO THE UNIT — and until this axis existed the
    /// second half was left to a systemd default nobody in this repo had written
    /// down, which is the same "declare the build, don't remember it" hole the
    /// ceiling itself had one setting earlier.
    ///
    /// # Why it is an AXIS and not a constant
    ///
    /// The two real services in this constellation want OPPOSITE answers, so there is
    /// no correct value to hardcode:
    ///
    /// * **A forge that spawns build children** (`dwarves-server`, which spawns
    ///   `rustc`): one overweight compile getting reaped must NOT take the server down
    ///   with it. The reaped process is a CHILD the forge can afford to lose, and the
    ///   right reaction is [`OomPolicy::Continue`] — dwarves' own `service.rs`
    ///   reasons the same way: *"the forge dies alone, with the box still up."*
    /// * **A service that IS the memory** (a warehouse server holding its index): if
    ///   its own main process is reaped, half a server is not a server. The right
    ///   reaction is [`OomPolicy::Stop`] — end in the `oom-kill` failed state and let
    ///   [`ServiceSpec::restart`] bring a whole one back, rather than limp on.
    ///
    /// # What an UNDECLARED reaction actually is
    ///
    /// Not "nothing happens". Per `man systemd.service`, an absent `OOMPolicy=` takes
    /// the manager-wide `DefaultOOMPolicy=`, except on `Delegate=yes` units which
    /// default to `continue`. MEASURED on t14s, systemd 259, 2026-08-25:
    /// `systemctl show -p DefaultOOMPolicy` ⇒ `stop`; `ssh.service` and
    /// `systemd-journald.service` ⇒ `OOMPolicy=stop` with `Delegate=no`;
    /// `user@1000.service` ⇒ `OOMPolicy=continue` with `Delegate=yes`. Skidbladnir
    /// renders no `Delegate=`, so its units inherit `DefaultOOMPolicy=` — a
    /// **per-box** setting an operator may change, which is exactly why a service
    /// with an opinion must declare it rather than inherit it.
    ///
    /// # Which backends honour it
    ///
    /// **Honoured, with a real native equivalent:**
    ///
    /// * **systemd — system AND user scope** — `OOMPolicy=continue|stop|kill`. Not a
    ///   privilege directive, so it is rendered in both scopes for the same reason
    ///   the ceiling is; a `--user` manager has an `OOMPolicy=` of its own.
    /// * **Quadlet** — the same `OOMPolicy=` directive, in the `[Service]` section,
    ///   which quadlet passes straight through to the generated `.service`. This is
    ///   the OPPOSITE side of the seam from the ceiling: `Memory=` had to go on
    ///   `[Container]` because it is the container's cgroup that must be bounded,
    ///   while the reaction is a decision the service manager makes about a UNIT and
    ///   podman has no equivalent flag — which is exactly why `Restart=` already lives
    ///   in `[Service]` here rather than becoming `--restart`.
    ///
    /// **NOT honoured — named in the output rather than dropped in silence:**
    ///
    /// * **OpenRC** — it can write cgroup-v2 attributes through `rc_cgroup_settings`,
    ///   which covers the `kill` half EXACTLY (systemd's own `kill` is documented as
    ///   "setting the `memory.oom.group` attribute to 1"), and covers `continue` and
    ///   `stop` NOT AT ALL: those two are service-MANAGER reactions to a kill event,
    ///   and `supervise-daemon` has no notion of an `oom-kill` failed state. Rendering
    ///   only the third of the axis it can reach would claim a policy axis OpenRC does
    ///   not have, so the note names `memory.oom.group` as the by-hand route instead.
    ///   (Not verified against a live OpenRC host — dwarf and t14s are systemd — which
    ///   is a second reason not to render it.)
    /// * **Kubernetes** — the pod API has no per-container OOM reaction.
    ///   `restartPolicy` is the RESTART axis (it says whether a dead container comes
    ///   back, never whether it should have died, so it cannot express `continue` at
    ///   all), and whether the kernel reaps one process or the container's whole
    ///   cgroup — `memory.oom.group`, which is what `kill` sets — is a KUBELET-wide
    ///   node setting, not a manifest field. Named in a comment at the render site
    ///   (`crate::k8s`, beside the `resources:` block) rather than approximated.
    /// * **runit**, **s6-rc**, **dinit**, **SysV**, **WindowsService** — no cgroup OOM
    ///   integration at all. Their rlimit knobs fail the allocation INSIDE the program
    ///   instead of having the kernel reap a cgroup, so there is no kill event for a
    ///   policy to react to (a stronger "cannot" than the ceiling's, which at least
    ///   had a near-miss to name).
    ///
    /// **Silent — a known residue, not a design:** the **OCI (podman)** renderer.
    /// podman has no policy flag (`--oom-kill-disable` is a cgroup-v1 setting that
    /// disables the kill rather than choosing a reaction to it, and podman refuses it
    /// on a cgroup-v2 host), and that backend's output is argv plus an env file — it
    /// has no comment channel a `NOT HONOURED` note could be written into. Declare
    /// against `Backend::Oci` and the reaction is currently lost without a word.
    /// Fixing it means giving `RenderedService` a notes channel, which is a change to
    /// every backend and is deliberately not smuggled in here.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub oom_policy: Option<OomPolicy>,
    /// **THE RATE LIMIT the restart cadence runs into** — `StartLimitIntervalSec=` +
    /// `StartLimitBurst=`, declared as one pair by [`StartLimit`] because neither
    /// number means anything alone.
    ///
    /// ── THE DEFECT, MEASURED 2026-08-25 ─────────────────────────────────────────
    /// This crate has always shipped `RestartSec=2` and never rendered a rate limit,
    /// so a unit inherited the box's `DefaultStartLimitBurst=5` /
    /// `DefaultStartLimitIntervalSec=10s`. systemd denies the (burst+1)-th start when
    /// it falls INSIDE the window and resets the window when it falls outside — and
    /// `2 * 5 == 10` puts it exactly on the line. On the live
    /// `dwarves-server.service` that meant **217830** restarts over 14 days, one run
    /// reaching counter **77998**, and **zero** `start-limit-hit` events: measured
    /// restart deltas of 2.066-2.250s (exec latency on top of the declared 2s) put
    /// every 6th attempt just past the 10s window. A faster box flips the same spec
    /// to parking permanently after five tries. The outcome was decided by process
    /// startup latency, not by anything anyone declared.
    ///
    /// `None` renders NOTHING, so an existing unit does not change by one byte — and
    /// then still inherits the box's window, which is the residual risk this field
    /// exists to let a service opt out of. See [`StartLimit::give_up_after`] and
    /// [`StartLimit::never_give_up`]; the pair is checked against
    /// [`restart_sec`](ServiceSpec::restart_sec) before anything is written.
    pub start_limit: Option<StartLimit>,
    /// **WHICH service the kernel picks when the BOX is out of memory** —
    /// `OOMScoreAdjust=`. The completion of the OOM story: the ceiling bounds one
    /// service, the reaction decides its fate once reaped, and this decides who gets
    /// reaped at all. On 2026-08-20 that was `gunnar-service`, taken alongside the
    /// forge that caused the shortage, because every unit sat at the same score.
    ///
    /// `None` renders nothing and inherits the manager's level (measured `0`). See
    /// [`OomScoreAdjust`] for the measured privilege asymmetry — raising is
    /// unprivileged, a negative bias needs `CAP_SYS_RESOURCE`.
    pub oom_score_adjust: Option<OomScoreAdjust>,
    /// Install scope — system service vs per-user service.
    pub scope: Scope,
    /// Working directory (`WorkingDirectory=` / OpenRC `directory=`).
    pub working_dir: Option<String>,
    /// **THE data root** — the ONE absolute directory this product keeps all of its
    /// state in, *the same whoever owns the process*.
    ///
    /// > *"make the data directory the SAME regardless of who fucking owns it"*
    ///
    /// Not derived from `$HOME`, not from the CWD, **not branched on
    /// [`ServiceSpec::scope`]**. It is what [`Lifecycle::uninstall`] erases (THE
    /// UNINSTALL LAW, [`crate::uninstall`]), and the resolver and the eraser read this
    /// same field — two separate ladders is exactly how dwarves and korp came to erase
    /// a path their own resolver never used, wiping **nothing** while printing
    /// success.
    ///
    /// Set it through [`ServiceSpec::data_root`], which takes a validated
    /// [`DataRoot`](crate::uninstall::DataRoot): a root that IS or CONTAINS a real
    /// user's home is refused where it is written. `None` ⇒ no data root is declared,
    /// the uninstall erases nothing, and the report says so.
    #[serde(default)]
    pub data_root: Option<String>,
    /// Pre-fixed-root data roots (a per-user `~/.dwarves`, `~/.nornir` from before the
    /// root was pinned) that an uninstall ALSO sweeps, by the same law, so a machine
    /// upgraded from the old layout ends up genuinely clean rather than keeping
    /// orphans no future uninstall can find. Each is named in the report.
    #[serde(default)]
    pub legacy_data_roots: Vec<String>,
    /// **THE DECLARED PROGRAM PATH(S)** — the directory (or directories) the installed
    /// binary this unit execs is allowed to live in.
    ///
    /// The project's rule is that every system declares an install path for the PROGRAM
    /// and one for the DATA. Only the data half was ever declared
    /// ([`data_root`](ServiceSpec::data_root)); nothing said where the program may live,
    /// and nothing checked.
    ///
    /// MEASURED on oden 2026-08-27: a product whose `--install` builds the unit around
    /// `std::env::current_exe` wrote `ExecStart=…/target/release/dwarves-server` when run
    /// from a build directory — a machine service executing out of a cargo target tree
    /// that the next `cargo clean` deletes. Declared here, projected onto the systemd
    /// limb, and REFUSED (not warned about) by
    /// `SystemdServiceSpec::validate_exec_is_durable` before any privileged step.
    ///
    /// Empty ⇒ undeclared, and only the build-tree refusal applies.
    #[serde(default)]
    pub program_dirs: Vec<String>,
    /// Sub-directories of [`ServiceSpec::data_root`] this service owns, for the rarer
    /// case where the root is shared with something else. Empty ⇒ sweep every child of
    /// the root (the normal case).
    #[serde(default)]
    pub state_dirs: Vec<String>,
    /// Environment, rendered inline AND/OR to the backend's env file.
    ///
    /// ⚠ **PUBLIC.** A systemd unit is world-readable (0644), so anything here is
    /// readable by every user on the box. Secrets go in [`ServiceSpec::secret_env`].
    pub env: Vec<(String, String)>,
    /// **Secret** environment — never rendered inline into the world-readable unit.
    ///
    /// `SystemdServiceSpec` has always had a private channel (`secret_environment`,
    /// written to a 0600 `EnvironmentFile=`, with a `validate()` that REFUSES a spec
    /// carrying secrets without one). The neutral `ServiceSpec` that every consumer
    /// actually builds had no such field, and `to_systemd_spec` only ever called
    /// `with_env` — so a consumer with a token to pass had exactly one place to put
    /// it, and that place rendered it into a 0644 file. The guard existed and nothing
    /// could reach it.
    ///
    /// Entries here route to the private channel. Backends with no private channel
    /// must REFUSE rather than silently downgrade to inline — a secret quietly
    /// rendered in the clear is worse than a build error.
    #[serde(default)]
    pub secret_env: Vec<(String, String)>,
    /// **Where the private env file goes** (systemd `EnvironmentFile=`, mode 0600).
    ///
    /// `None` ⇒ derived: `/etc/<name>/<name>.env` when [`ServiceSpec::secret_env`] is
    /// non-empty, no file otherwise. In user scope only the FILE NAME is used — the
    /// file lives next to the unit in `~/.config/systemd/user/` (see
    /// [`crate::systemd::env_file_location`]), so the same spec installs at both scopes.
    #[serde(default)]
    pub env_file: Option<String>,
    /// **OPERATOR environment — what THIS deployment was told, as opposed to what the
    /// product ships.**
    ///
    /// Rendered after [`ServiceSpec::env`], so for a key both declare the operator's
    /// value wins. Its reason for being a separate axis is persistence: an install that
    /// is given none reads this set back out of the unit it is replacing and carries it
    /// forward (see [`crate::systemd::operator_env_from_unit_text`]). That is the one
    /// property a hand-written `<unit>.service.d/*.conf` had and an install did not —
    /// and the reason three of oden's korp-server settings lived in drop-ins that no
    /// installer wrote and no uninstall removed.
    ///
    /// ⚠ **PUBLIC**, exactly like [`ServiceSpec::env`]. Secrets go in
    /// [`ServiceSpec::secret_env`].
    #[serde(default)]
    pub operator_env: Vec<(String, String)>,
    /// **Configuration files the service reads**, shipped with the spec instead of
    /// baked into the binary's package or the container image. Every host backend
    /// renders each one as a file at its absolute `path`. Kubernetes puts them all in
    /// a `<name>-config` ConfigMap, mounts each at its `path` (read-only, `subPath`),
    /// and stamps the pod template with their hash, so a changed file rolls the
    /// workload. Not a secret channel: secrets go in [`ServiceSpec::secret_env`].
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub config_files: Vec<ConfigFile>,
    /// Services this one must start *after* (ordering edges).
    pub after: Vec<String>,
    /// Services this one *hard-needs* (start fails if they are down).
    pub needs: Vec<String>,
    /// Restart policy (maps to each backend's supervision knob).
    pub restart: RestartPolicy,
    /// Restart backoff seconds.
    pub restart_sec: u32,
    /// Sockets the service listens on (socket activation / declared ports).
    pub sockets: Vec<Socket>,
    /// How [`Lifecycle::health`] verifies the started service is up.
    #[serde(default)]
    pub health: HealthProbe,
    /// **Where this service's stdout+stderr are appended** — the READINESS MARKER
    /// channel. Projects onto
    /// [`SystemdServiceSpec::stdout_file`](crate::systemd::SystemdServiceSpec::stdout_file)
    /// (`StandardOutput=append:` + `StandardError=append:`); read back by
    /// [`crate::service_drive::ServiceDrive::await_marker`].
    ///
    /// systemd-only. Every OTHER [`ServiceRenderer`] in this module ignores it, and
    /// says so rather than pretending: OpenRC/sysvinit/runit/dinit/s6/OCI/Quadlet/
    /// Windows each have their own redirection spelling and guessing five of them
    /// from one field is how a value gets silently lost. `None` (the default) is the
    /// shape every existing spec has and changes nothing anywhere.
    #[serde(default)]
    pub stdout_file: Option<String>,
    /// Fat (server, holds state) vs thin (routes to a fat peer) — the nornir
    /// server/thin-client split, so holger/dwarfs inherit it once they declare a
    /// spec. Metadata only; it does not change the rendered unit.
    #[serde(default)]
    pub agent: AgentMode,

    // ── Kubernetes projection (ignored by the init-system renderers) ──
    /// Container image for the k8s render. A k8s workload needs an image, not a
    /// host `exec` path — `None` means this spec can't be expressed on k8s.
    #[serde(default)]
    pub image: Option<String>,
    /// Target namespace (k8s render). `None` ⇒ `default`.
    #[serde(default)]
    pub namespace: Option<String>,
    /// Desired replica count (k8s render).
    #[serde(default = "one")]
    pub replicas: u32,
    /// Stateful ⇒ render a StatefulSet instead of a Deployment (k8s render).
    #[serde(default)]
    pub stateful: bool,
    /// Persistent storage ⇒ emit a PVC + volume mount (k8s render).
    #[serde(default)]
    pub storage: Option<Storage>,
    /// k8s Secret names attached to the container env via `envFrom.secretRef`
    /// (Kubernetes render only). Empty ⇒ no `envFrom` block.
    #[serde(default)]
    pub env_from_secrets: Vec<String>,
}

fn one() -> u32 {
    1
}

/// **Parse every `--env KEY=VALUE` out of an argv — the ONE parser every product's
/// install verb uses.**
///
/// The flag is repeatable and order is preserved. It exists because an operator
/// decision belongs in the DECLARATION the installer writes, not in a hand-written
/// `<unit>.service.d/*.conf` that nothing owns, no uninstall removes and every future
/// install re-applies blind. Feed the result to
/// [`ServiceSpec::operator_env`](crate::service::ServiceSpec::operator_env), which the
/// install then carries forward for you.
///
/// # A malformed pair is an ERROR, never a shrug
///
/// `--env DWARVES_REAL_RUNNER` (no `=`) silently dropped installs a unit missing
/// exactly the variable the operator ran the command for, and prints success. So does
/// a trailing `--env` with nothing after it. Both are refused loudly.
///
/// An empty VALUE is legal (`--env FOO=` renders `Environment=FOO=`): systemd treats
/// that as "set but empty", which is a real thing to want. An empty KEY is not.
///
/// Lives here rather than in a product bin because dwarves wrote the first copy and
/// korp needed the second (LAW 5 — the ship owns service lifecycle, and the argv that
/// configures a service is part of it).
pub fn env_args_from_argv<S: AsRef<str>>(argv: &[S]) -> anyhow::Result<Vec<(String, String)>> {
    let mut out: Vec<(String, String)> = Vec::new();
    let mut i = 0;
    while i < argv.len() {
        let arg = argv[i].as_ref();
        // Both spellings, so `--env=K=V` is not silently ignored by a parser that only
        // knows the spaced form.
        let pair: Option<&str> = if arg == "--env" {
            let next = argv.get(i + 1).map(|a| a.as_ref()).ok_or_else(|| {
                anyhow::anyhow!(
                    "--env needs a KEY=VALUE pair (e.g. --env KORP_SERVER_ADDR=0.0.0.0:50051)"
                )
            })?;
            i += 1;
            Some(next)
        } else {
            arg.strip_prefix("--env=")
        };
        if let Some(pair) = pair {
            let (k, v) = pair.split_once('=').ok_or_else(|| {
                anyhow::anyhow!(
                    "--env `{pair}` is not KEY=VALUE — it has no `=`. Dropping it would \
                     install a unit missing exactly the variable you asked for."
                )
            })?;
            if k.is_empty() {
                anyhow::bail!("--env `{pair}` has an empty KEY");
            }
            out.push((k.to_string(), v.to_string()));
        }
        i += 1;
    }
    Ok(out)
}

/// Persistent storage request for the k8s render — a PVC of `size` mounted at
/// `mount_path` (e.g. `8Gi` at `/var/lib/postgresql`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Storage {
    pub size: String,
    pub mount_path: String,
}

impl Storage {
    pub fn new(size: impl Into<String>, mount_path: impl Into<String>) -> Self {
        Storage {
            size: size.into(),
            mount_path: mount_path.into(),
        }
    }
}

impl ServiceSpec {
    /// A spec with sane defaults: system scope, restart on-failure (2s), no deps.
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        exec: impl Into<String>,
    ) -> Self {
        ServiceSpec {
            name: name.into(),
            description: description.into(),
            exec: exec.into(),
            args: Vec::new(),
            user: None,
            group: None,
            program_dirs: Vec::new(),
            supplementary_groups: Vec::new(),
            memory: None,
            oom_policy: None,
            start_limit: None,
            oom_score_adjust: None,
            scope: Scope::System,
            working_dir: None,
            data_root: None,
            legacy_data_roots: Vec::new(),
            state_dirs: Vec::new(),
            env: Vec::new(),
            secret_env: Vec::new(),
            env_file: None,
            operator_env: Vec::new(),
            config_files: Vec::new(),
            env_from_secrets: Vec::new(),
            after: Vec::new(),
            needs: Vec::new(),
            restart: RestartPolicy::OnFailure,
            restart_sec: 2,
            sockets: Vec::new(),
            health: HealthProbe::None,
            stdout_file: None,
            agent: AgentMode::Fat,
            image: None,
            namespace: None,
            replicas: 1,
            stateful: false,
            storage: None,
        }
    }

    pub fn image(mut self, i: impl Into<String>) -> Self {
        self.image = Some(i.into());
        self
    }
    pub fn namespace(mut self, n: impl Into<String>) -> Self {
        self.namespace = Some(n.into());
        self
    }
    pub fn replicas(mut self, r: u32) -> Self {
        self.replicas = r;
        self
    }
    pub fn stateful(mut self, s: bool) -> Self {
        self.stateful = s;
        self
    }
    pub fn storage(mut self, s: Storage) -> Self {
        self.storage = Some(s);
        self
    }

    pub fn arg(mut self, a: impl Into<String>) -> Self {
        self.args.push(a.into());
        self
    }
    pub fn user(mut self, u: impl Into<String>) -> Self {
        self.user = Some(u.into());
        self
    }
    pub fn group(mut self, g: impl Into<String>) -> Self {
        self.group = Some(g.into());
        self
    }
    /// Declare **THE PROGRAM PATH(S)** — see
    /// [`program_dirs`](ServiceSpec::program_dirs). Replaces any previous declaration.
    ///
    /// The counterpart of [`data_root`](ServiceSpec::data_root): one declaration for the
    /// program, one for the data, both riding the SAME projection onto the systemd limb
    /// so the two halves of "where does this system live" cannot drift.
    pub fn with_program_dirs<I, S>(mut self, dirs: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.program_dirs = dirs.into_iter().map(Into::into).collect();
        self
    }
    /// **An ADDITIONAL group the service process joins** — systemd
    /// `SupplementaryGroups=`. Repeatable; order is preserved.
    ///
    /// # Why a declaration and not a drop-in
    ///
    /// A service that must open a resource owned by *another* account's group has
    /// exactly one non-root answer on Linux: be a member of that group.
    /// [`ServiceSpec::group`] cannot express it — that is the PRIMARY group, one
    /// value, and a service normally wants its own. So until this existed, the only
    /// route was an operator hand-writing
    /// `/etc/systemd/system/<unit>.service.d/*.conf` — a file no install writes, no
    /// uninstall removes, and every future `--refresh` renders around without
    /// knowing it is there. That is the exact shape of the three settings found
    /// living in unowned drop-ins on oden.
    ///
    /// # The case that produced it, MEASURED on oden 2026-08-11
    ///
    /// korp-server runs `User=korp` (uid 111, `nologin`, no lingering) and routes
    /// FalkorDB and Spark through draupnir's OCI engine, which needs the podman API
    /// socket. `podman.socket` was **active**, `sudo podman ps` listed a running
    /// container, and korp still could not connect: `/run/podman/podman.sock` is
    /// `root:root 0660` inside `/run/podman` `root:root 0700`, so uid 111 was
    /// refused at the directory. Nothing about that is fixable by enabling a unit;
    /// it is group membership, and the unit had no way to ask for it.
    ///
    /// # What this does NOT do
    ///
    /// It cannot invent a group. `SupplementaryGroups=` names a group that must
    /// exist and must actually own the resource — if the socket's group is `root`,
    /// this directive has no honest value to carry (joining `root` is not a fix),
    /// and the answer is a host-side `SocketGroup=` or running as root. draupnir's
    /// socket diagnosis says which of the two situations a box is in.
    pub fn supplementary_group(mut self, g: impl Into<String>) -> Self {
        self.supplementary_groups.push(g.into());
        self
    }
    /// Declare **THE resource ceiling** on this service — a soft ceiling
    /// (`MemoryHigh=`: throttled and reclaimed, the service gets SLOW), a hard
    /// ceiling (`MemoryMax=`: THIS cgroup is OOM-killed and [`ServiceSpec::restart`]
    /// brings it back), or both. See [`ServiceSpec::memory`] for the 2026-08-20
    /// incident this axis exists for and the full per-backend map.
    ///
    /// Takes an already-validated [`MemoryBound`], so an impossible declaration
    /// (`MemoryMax=` below `MemoryHigh=`, or a zero ceiling) is refused where the
    /// numbers are typed — the [`crate::uninstall::DataRoot`] posture, not a check
    /// buried in an install that has already written a unit around it.
    ///
    /// ```
    /// use skidbladnir_systemd::service::{MemoryBound, ServiceSpec};
    /// // The bound the hand-written 10-bounded.conf was carrying, declared instead.
    /// let unit = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/dwarves-server")
    ///     .user("dwarves")
    ///     .memory(MemoryBound::gib(16, 24).unwrap())
    ///     .render_systemd_unit();
    /// assert!(unit.contains("MemoryHigh=16G\n") && unit.contains("MemoryMax=24G\n"));
    /// ```
    pub fn memory(mut self, bound: MemoryBound) -> Self {
        self.memory = Some(bound);
        self
    }
    /// Declare **THE REACTION** to one of this service's processes being OOM-killed
    /// — `continue` (log it, the unit keeps running), `stop` (terminate the unit
    /// cleanly and let [`ServiceSpec::restart`] bring it back) or `kill` (reap every
    /// remaining process too). See [`ServiceSpec::oom_policy`] for why the axis
    /// exists, what an UNDECLARED reaction falls back to, and which backends honour
    /// it.
    ///
    /// Takes an [`OomPolicy`], never a string: `OOMPolicy=` has three legal
    /// spellings and systemd's parser is case-sensitive, so a typo does not degrade
    /// the directive — it makes the whole unit fail to load. The one string-facing
    /// seam is `OomPolicy::from_str`, which refuses by name.
    ///
    /// ```
    /// use skidbladnir_systemd::service::{MemoryBound, OomPolicy, ServiceSpec};
    /// // A forge that spawns build children: a reaped `rustc` must not take the
    /// // server down with it.
    /// let unit = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/dwarves-server")
    ///     .user("dwarves")
    ///     .memory(MemoryBound::gib(16, 24).unwrap())
    ///     .oom_policy(OomPolicy::Continue)
    ///     .render_systemd_unit();
    /// assert!(unit.contains("OOMPolicy=continue\n"));
    /// ```
    pub fn oom_policy(mut self, policy: OomPolicy) -> Self {
        self.oom_policy = Some(policy);
        self
    }

    /// Declare **THE RATE LIMIT** the restart cadence runs into. See
    /// [`start_limit`](ServiceSpec::start_limit) for the 217830-restart measurement
    /// that produced this axis, and [`StartLimit`] for why the pair is computed from
    /// an intent rather than typed as two raw numbers.
    ///
    /// ```
    /// use skidbladnir_systemd::service::{ServiceSpec, StartLimit};
    /// let spec = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d")
    ///     .restart_sec(2)
    ///     .start_limit(StartLimit::give_up_after(5, 2).unwrap());
    /// let unit = spec.render_systemd_unit();
    /// assert!(unit.contains("StartLimitIntervalSec=12"));
    /// assert!(unit.contains("StartLimitBurst=5"));
    /// ```
    ///
    /// **Reading the cadence off the spec needs two lines**, because the builder
    /// consumes `self` and Rust evaluates the receiver before the argument — so
    /// `spec.start_limit(StartLimit::give_up_after(5, spec.restart_sec))` is
    /// `E0382: use of moved value`, hit for real the first time this was wired up:
    ///
    /// ```
    /// use skidbladnir_systemd::service::{ServiceSpec, StartLimit};
    /// let spec = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d");
    /// let cadence = spec.restart_sec;               // read it out FIRST
    /// let spec = spec.start_limit(StartLimit::give_up_after(5, cadence).unwrap());
    /// assert!(spec.render_systemd_unit().contains("StartLimitIntervalSec=12"));
    /// ```
    ///
    /// Or skip the dance entirely with [`give_up_after`](ServiceSpec::give_up_after),
    /// which reads the cadence off the spec itself.
    pub fn start_limit(mut self, limit: StartLimit) -> Self {
        self.start_limit = Some(limit);
        self
    }

    /// **Give up after `attempts` restarts, reconciled against THIS spec's own
    /// cadence** — the one-line form of
    /// [`start_limit`](ServiceSpec::start_limit) + [`StartLimit::give_up_after`].
    ///
    /// It exists because the two-argument form makes a caller name the cadence twice,
    /// and the obvious way to avoid that does not compile: the builder consumes
    /// `self`, so `spec.start_limit(StartLimit::give_up_after(5, spec.restart_sec))`
    /// is `E0382: use of moved value` (the receiver is evaluated before the argument).
    /// Naming it twice is worse than a compile error, though — the whole axis exists
    /// because a cadence and its rate limit were set in two places and never
    /// reconciled, so a second literal is the same defect in miniature.
    ///
    /// Order still matters, and [`SystemdServiceSpec::validate`] still catches it: set
    /// the cadence FIRST, or the limit is computed against the old one. That is a
    /// refusal by name, not a silent miscalculation.
    ///
    /// ```
    /// use skidbladnir_systemd::service::ServiceSpec;
    /// let spec = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d")
    ///     .restart_sec(2)
    ///     .give_up_after(5);
    /// let unit = spec.render_systemd_unit();
    /// assert!(unit.contains("StartLimitIntervalSec=12"));
    /// assert!(unit.contains("StartLimitBurst=5"));
    /// ```
    ///
    /// # Panics
    /// Only if `attempts` and the spec's cadence cannot form a decided pair, which for
    /// any cadence >= 1 and any `attempts` they always can — the computed interval is
    /// `restart_sec * (attempts + 1)`, a full restart period clear of the boundary by
    /// construction.
    pub fn give_up_after(self, attempts: u32) -> Self {
        let cadence = self.restart_sec;
        let limit = StartLimit::give_up_after(attempts, cadence)
            .expect("give_up_after computes a decided pair by construction");
        self.start_limit(limit)
    }

    /// Declare **WHICH service the kernel reaches for first**. See
    /// [`oom_score_adjust`](ServiceSpec::oom_score_adjust) and [`OomScoreAdjust`].
    pub fn oom_score_adjust(mut self, adj: OomScoreAdjust) -> Self {
        self.oom_score_adjust = Some(adj);
        self
    }

    /// The restart cadence — `RestartSec=`, seconds. Defaults to 2.
    ///
    /// Paired with [`start_limit`](ServiceSpec::start_limit): these two decide between
    /// "park in `failed`" and "retry forever", and the default 2 is exactly the value
    /// that makes the INHERITED window undecidable. A builder for it exists so both
    /// halves can be declared in one chain, where a reader can see them together.
    pub fn restart_sec(mut self, secs: u32) -> Self {
        self.restart_sec = secs;
        self
    }
    pub fn scope(mut self, s: Scope) -> Self {
        self.scope = s;
        self
    }
    /// Set the health probe [`Lifecycle::health`] uses.
    pub fn health(mut self, h: HealthProbe) -> Self {
        self.health = h;
        self
    }
    /// Append this service's stdout+stderr to `path` — see
    /// [`stdout_file`](ServiceSpec::stdout_file). systemd only.
    pub fn stdout_file(mut self, path: impl Into<String>) -> Self {
        self.stdout_file = Some(path.into());
        self
    }
    /// Set the fat/thin agent mode (default [`AgentMode::Fat`]).
    pub fn agent(mut self, a: AgentMode) -> Self {
        self.agent = a;
        self
    }
    pub fn working_dir(mut self, d: impl Into<String>) -> Self {
        self.working_dir = Some(d.into());
        self
    }
    /// Declare **THE data root** — the one absolute directory this product keeps all
    /// of its state in, the same whoever owns the process. This is what
    /// [`Lifecycle::uninstall`] erases.
    ///
    /// Takes an already-validated [`DataRoot`](crate::uninstall::DataRoot), so a spec
    /// **cannot be constructed** with a root that is (or contains) a real user's home,
    /// a system directory, a relative path, or something one component deep — the
    /// configuration error fails where it is written:
    ///
    /// ```no_run
    /// use skidbladnir_systemd::{service::ServiceSpec, uninstall::DataRoot};
    /// let root = DataRoot::new("/var/lib/dwarves")?;      // validated HERE
    /// let spec = ServiceSpec::new("dwarves", "Dwarves", "/usr/local/bin/dwarves")
    ///     .data_root(&root);
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// For a dev-box override use [`DataRoot::from_env`](crate::uninstall::DataRoot::from_env)
    /// — it feeds this same single declaration, never a second path ladder.
    pub fn data_root(mut self, root: &crate::uninstall::DataRoot) -> Self {
        self.data_root = Some(root.path().display().to_string());
        self
    }
    /// Declare a pre-fixed-root data root an uninstall should ALSO sweep, so a machine
    /// upgraded from the old per-user layout ends up genuinely clean. See
    /// [`ServiceSpec::legacy_data_roots`]. Validated at plan time; a legacy root that
    /// is a real user's home is refused and REPORTED, never swept.
    pub fn legacy_data_root(mut self, d: impl Into<String>) -> Self {
        self.legacy_data_roots.push(d.into());
        self
    }
    /// Declare a state sub-directory of [`ServiceSpec::data_root`] this service owns —
    /// only needed when the root is shared. Empty (the normal case) sweeps every child
    /// of the root.
    pub fn state_dir(mut self, d: impl Into<String>) -> Self {
        self.state_dirs.push(d.into());
        self
    }
    pub fn env(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.env.push((k.into(), v.into()));
        self
    }
    /// Declare an **operator** environment variable — a value this deployment chose,
    /// which the next install carries forward when it is not repeated. See
    /// [`ServiceSpec::operator_env`]. Public (0644), never a secret.
    pub fn operator_env(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.operator_env.push((k.into(), v.into()));
        self
    }

    /// Declare a **secret** environment variable — routed to the backend's private
    /// channel (systemd: a 0600 `EnvironmentFile=`), never inline in the unit.
    ///
    /// Use this for tokens, passwords and keys. [`ServiceSpec::env`] renders into a
    /// world-readable 0644 unit and is for public configuration only.
    pub fn secret_env(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.secret_env.push((k.into(), v.into()));
        self
    }
    /// The environment a backend writes to its PRIVATE (0600) env file: the public
    /// [`ServiceSpec::env`] followed by [`ServiceSpec::secret_env`], so a secret wins
    /// a key collision exactly as it does in the systemd env file.
    pub fn file_env(&self) -> Vec<(&str, &str)> {
        self.env
            .iter()
            .chain(&self.secret_env)
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect()
    }

    /// **Refuse a spec no backend can render safely** (stinky #7/#17) — pure, and the
    /// first thing every [`ServiceRenderer::render`] does. The name becomes a file name
    /// on every backend (`/etc/init.d/<name>`, `<unit_dir>/<name>.service`, …), so it
    /// must be a valid unit name (no `/`, no `..`); single-line fields must not carry a
    /// control character (a newline injects a line into a unit or a sourced script);
    /// env keys must be variable names. Args and env VALUES may hold anything — every
    /// backend quotes those, or refuses what its format cannot express.
    pub fn validate(&self) -> anyhow::Result<()> {
        use crate::systemd::{check_account_name, check_env_key, check_no_control, check_service_name};
        let ctx = |e: String| anyhow::anyhow!("service `{}`: {e}", self.name.escape_debug());
        check_service_name(&self.name).map_err(ctx)?;
        let singles = [
            ("description", Some(&self.description)),
            ("exec", Some(&self.exec)),
            ("working_dir", self.working_dir.as_ref()),
            ("data_root", self.data_root.as_ref()),
            ("env_file", self.env_file.as_ref()),
            ("stdout_file", self.stdout_file.as_ref()),
            ("image", self.image.as_ref()),
            ("namespace", self.namespace.as_ref()),
        ];
        for (what, v) in singles {
            if let Some(v) = v {
                check_no_control(what, v).map_err(ctx)?;
            }
        }
        for v in self
            .legacy_data_roots
            .iter()
            .chain(&self.program_dirs)
            .chain(&self.state_dirs)
            .chain(&self.after)
            .chain(&self.needs)
            .chain(&self.env_from_secrets)
        {
            check_no_control("path/unit/secret reference", v).map_err(ctx)?;
        }
        if let Some(st) = &self.storage {
            check_no_control("storage mount_path", &st.mount_path).map_err(ctx)?;
            check_no_control("storage size", &st.size).map_err(ctx)?;
        }
        if let Some(u) = &self.user {
            check_account_name("user", u).map_err(ctx)?;
        }
        if let Some(g) = &self.group {
            check_account_name("group", g).map_err(ctx)?;
        }
        for g in &self.supplementary_groups {
            check_account_name("supplementary group", g).map_err(ctx)?;
        }
        for (k, _) in self.env.iter().chain(&self.secret_env).chain(&self.operator_env) {
            check_env_key(k).map_err(ctx)?;
        }
        ConfigFile::check_all(&self.config_files).map_err(ctx)?;
        Ok(())
    }

    /// Ship a configuration file with the service (mode 0644); see
    /// [`ServiceSpec::config_files`].
    pub fn config_file(mut self, path: impl Into<String>, contents: impl Into<String>) -> Self {
        self.config_files.push(ConfigFile {
            path: path.into(),
            contents: contents.into(),
            mode: ConfigFile::DEFAULT_MODE,
        });
        self
    }

    /// Declare where the private env file goes (see [`ServiceSpec::env_file`]).
    pub fn env_file(mut self, path: impl Into<String>) -> Self {
        self.env_file = Some(path.into());
        self
    }
    /// Attach a k8s Secret to the container env via `envFrom.secretRef`
    /// (Kubernetes render only).
    pub fn env_from_secret(mut self, name: impl Into<String>) -> Self {
        self.env_from_secrets.push(name.into());
        self
    }
    pub fn after(mut self, dep: impl Into<String>) -> Self {
        self.after.push(dep.into());
        self
    }
    pub fn needs(mut self, dep: impl Into<String>) -> Self {
        self.needs.push(dep.into());
        self
    }
    pub fn restart(mut self, p: RestartPolicy) -> Self {
        self.restart = p;
        self
    }
    pub fn socket(mut self, s: Socket) -> Self {
        self.sockets.push(s);
        self
    }

    /// The full command line as POSIX-shell words: `exec` then each arg, every word
    /// [`sh_quote`]d, so `sh` hands the process exactly `[exec, args…]` (stinky #10).
    pub fn command_line(&self) -> String {
        sh_words(std::iter::once(self.exec.as_str()).chain(self.args.iter().map(String::as_str)))
    }

    /// Render THIS spec straight to a systemd `.service` unit for its own
    /// [`ServiceSpec::scope`], through the ONE renderer
    /// [`crate::systemd::systemd_unit_scoped`]. The one-call convenience a consumer
    /// (nornir's server unit) uses so it never re-hand-rolls the unit text — it
    /// declares a spec and calls this. Equivalent to
    /// `renderer_for(systemd-backend).render(self)`, returning just the unit body.
    pub fn render_systemd_unit(&self) -> String {
        crate::systemd::systemd_unit_scoped(&to_systemd_spec(self), self.scope)
    }
}

/// **POSIX-shell quote one word** (stinky #10): bare when every byte is one the shell
/// never interprets, else `'…'` with each `'` as `'\''` — the one quoting that has no
/// expansion inside it (`$(…)`, backticks, `$VAR`, `\`, globs all stay literal).
pub fn sh_quote(word: &str) -> String {
    let bare = !word.is_empty()
        && word
            .bytes()
            .all(|b| b.is_ascii_alphanumeric() || b"_@%+=:,./-".contains(&b));
    if bare {
        word.to_string()
    } else {
        format!("'{}'", word.replace('\'', "'\\''"))
    }
}

/// [`sh_quote`] each word and join with spaces — a command line `sh` splits back into
/// exactly these words.
pub fn sh_words<'a>(words: impl IntoIterator<Item = &'a str>) -> String {
    words.into_iter().map(sh_quote).collect::<Vec<_>>().join(" ")
}

/// One dinit command-line token: double-quoted, `\` and `"` backslash-escaped and `$`
/// doubled (dinit substitutes `$NAME` even inside quotes; `$$` is a literal `$`).
fn dinit_word(word: &str) -> String {
    let mut out = String::with_capacity(word.len() + 2);
    out.push('"');
    for c in word.chars() {
        match c {
            '\\' | '"' => {
                out.push('\\');
                out.push(c);
            }
            '$' => out.push_str("$$"),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// One `NAME=VALUE` line of a LITERAL env file (dinit `env-file`, podman
/// `--env-file`): neither format has quoting, so a value with a line break or NUL
/// cannot be expressed and is REFUSED rather than split into a second, injected line.
fn literal_env_line(format: &str, k: &str, v: &str) -> anyhow::Result<String> {
    if v.contains(['\n', '\r', '\0']) {
        anyhow::bail!(
            "{format}: env `{k}` holds a line break or NUL, which a literal NAME=VALUE env file cannot carry"
        );
    }
    Ok(format!("{k}={v}\n"))
}

/// One Windows command-line argument, quoted by the `CommandLineToArgvW` rules:
/// bare when it has no space, tab or `"`; else `"…"` with `"` as `\"` and backslashes
/// doubled only where they precede a `"` (or the closing quote).
#[doc(hidden)]
pub fn windows_arg(arg: &str) -> String {
    if !arg.is_empty() && !arg.contains([' ', '\t', '"']) {
        return arg.to_string();
    }
    let mut out = String::from("\"");
    let mut slashes = 0usize;
    for c in arg.chars() {
        match c {
            '\\' => slashes += 1,
            '"' => {
                out.extend(std::iter::repeat_n('\\', slashes * 2 + 1));
                out.push('"');
                slashes = 0;
            }
            c => {
                out.extend(std::iter::repeat_n('\\', slashes));
                out.push(c);
                slashes = 0;
            }
        }
    }
    out.extend(std::iter::repeat_n('\\', slashes * 2));
    out.push('"');
    out
}

/// Project the backend-neutral [`ServiceSpec`](crate::service::ServiceSpec) onto the systemd-specific
/// [`crate::systemd::SystemdServiceSpec`] the ONE renderer
/// ([`crate::systemd::systemd_unit_scoped`]) consumes. Shared by
/// [`SystemdRenderer`](crate::service::SystemdRenderer), [`Lifecycle`] and [`ServiceSpec::render_systemd_unit`] so
/// there is a SINGLE neutral→systemd mapping (no second projection to drift).
#[doc(hidden)]
pub fn to_systemd_spec(spec: &ServiceSpec) -> crate::systemd::SystemdServiceSpec {
    // user scope runs as the caller → no User=; system scope needs a user
    // (fall back to the service name if none was set).
    let user = spec.user.clone().unwrap_or_else(|| spec.name.clone());
    let mut s = crate::systemd::SystemdServiceSpec::new(
        &spec.name,
        &spec.description,
        &spec.exec,
        user,
        // RENDER-ONLY default: a spec that declares no working dir still needs a
        // `WorkingDirectory=` line, and `/` is systemd's own default.
        spec.working_dir.clone().unwrap_or_else(|| "/".to_string()),
    )
    // …but the SERVICE HOME (the only thing the privileged install creates and
    // chowns) is the DECLARED working dir and nothing else. INCIDENT 2026-07-22:
    // the `/` render default leaked through `SystemdServiceSpec::new` into
    // `ensure_system_user`, which recursively re-owned the filesystem root. A spec
    // that declares no dir must declare NO home.
    .with_service_home(spec.working_dir.clone())
    .with_restart(spec.restart);
    s.restart_sec = spec.restart_sec;
    // The ONE data-root declaration rides along, so the systemd limb's uninstall
    // erases the same directory the neutral `Lifecycle` resolves — one value, not two
    // ladders.
    s.data_root = spec.data_root.clone();
    s.legacy_data_roots = spec.legacy_data_roots.clone();
    // …and the PROGRAM path, down the SAME one projection as the data path. The two
    // halves of "where does this system live" must arrive by one route or they drift,
    // which is the whole reason this projection is the only ladder.
    s.program_dirs = spec.program_dirs.clone();
    s.state_dirs = spec.state_dirs.clone();
    // Explicit group override (else SystemdServiceSpec defaults Group= to User=).
    s.group = spec.group.clone();
    // …and the ADDITIONAL groups, carried on their own axis right beside the primary
    // one so the two cannot drift. `Group=` is a single value and a service wants its
    // own; `SupplementaryGroups=` is how it also reaches somebody else's resource.
    s.supplementary_groups = spec.supplementary_groups.clone();
    s.config_files = spec.config_files.clone();
    // …and THE RESOURCE CEILING, on its own axis, through this ONE projection. There
    // is no second neutral→systemd ladder for it to drift down: `SystemdRenderer`,
    // `Lifecycle` and `ServiceSpec::render_systemd_unit` all arrive here, so a declared
    // soft/hard ceiling reaches the rendered unit by exactly one path — which is the
    // property the hand-written `10-bounded.conf` never had.
    s.memory = spec.memory;
    // …and THE REACTION to that ceiling being hit, down the SAME single projection.
    // The two are one story and must not be able to arrive by different routes: a
    // unit carrying a ceiling from one path and a reaction from another is how the
    // pair silently drifts apart.
    s.oom_policy = spec.oom_policy;
    // …and the rate limit, through the SAME one path. The `[Unit]` half of the pair
    // (`RestartSec` already rides `restart_sec` a few lines up), so both halves reach
    // the renderer together and `SystemdServiceSpec::validate` can judge them together.
    s.start_limit = spec.start_limit;
    s.oom_score_adjust = spec.oom_score_adjust;
    // The marker channel rides down to the systemd limb on its own field, so the ONE
    // unit writer is still the only thing that renders `StandardOutput=`.
    s.stdout_file = spec.stdout_file.clone();
    for a in &spec.args {
        s = s.with_arg(a);
    }
    for (k, v) in &spec.env {
        s = s.with_env(k, v);
    }
    // Operator values ride a SEPARATE axis all the way down, so the install can tell a
    // deployment's own decision from a product default and carry only the first forward.
    for (k, v) in &spec.operator_env {
        s = s.with_operator_env(k, v);
    }
    // Secrets go to the PRIVATE channel (a 0600 `EnvironmentFile=`), never inline.
    // Until this loop existed, `ServiceSpec` had no secret field at all and every
    // value a consumer passed went through `with_env` above — straight into the
    // world-readable 0644 unit. `SystemdServiceSpec::validate` refuses a spec that
    // carries secrets without an env_file, so an over-eager caller gets a loud error
    // rather than a quiet downgrade.
    for (k, v) in &spec.secret_env {
        s = s.with_secret_env(k, v);
    }
    // …and they get somewhere private to GO. Without this, `validate` refused every
    // `Lifecycle::install` that carried a secret, and each consumer re-derived the path.
    s.env_file = spec.env_file.clone().or_else(|| {
        (!spec.secret_env.is_empty()).then(|| format!("/etc/{0}/{0}.env", spec.name))
    });
    // ordering + hard requires both become After=; needs additionally Wants=.
    for dep in &spec.after {
        s = s.after(unit_name(dep));
    }
    for dep in &spec.needs {
        s = s.after(unit_name(dep));
        s = s.wants(unit_name(dep));
    }
    s
}

/// One file a renderer emits, with its install path (relative to the backend's
/// root) and unix mode. Pure data — the privileged `apply()` writes it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenderedFile {
    /// Install path. Absolute for system scope (e.g.
    /// `/etc/systemd/system/holger.service`); a renderer may also emit a path
    /// relative to `$HOME` for user scope — documented per renderer.
    pub path: String,
    /// File body.
    pub contents: String,
    /// Unix mode (e.g. `0o644` units, `0o755` scripts, `0o600` env files).
    pub mode: u32,
}

/// The full output of rendering a [`ServiceSpec`] for one backend: the file(s) to
/// write **plus the lifecycle verbs as data** (argv vectors). Capturing the verbs
/// as data — not hidden inside an `apply()` — is what lets a test assert "OpenRC
/// enables with `rc-update add holger default`" without running anything, and lets
/// a single privileged driver invoke them uniformly across backends.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenderedService {
    pub backend: Backend,
    pub files: Vec<RenderedFile>,
    /// argv to enable (start-at-boot) the service.
    pub enable: Vec<String>,
    /// argv to start it now.
    pub start: Vec<String>,
    /// argv to query status.
    pub status: Vec<String>,
    /// argv to stop it.
    pub stop: Vec<String>,
    /// argv that undoes `enable` on uninstall, run after `stop`: it takes the
    /// service out of boot (systemd `disable`, `rc-update del`, `update-rc.d -f …
    /// remove`, unlinking the runit scan link, …), deletes the container or SCM
    /// service, or deletes every object the manifests created from the cluster
    /// (k8s, by kind/name, so it does not need the manifest files).
    pub remove: Vec<String>,
}

impl RenderedService {
    /// The body of the first file whose path ends with `suffix` (test helper).
    pub fn file_ending(&self, suffix: &str) -> Option<&str> {
        self.files
            .iter()
            .find(|f| f.path.ends_with(suffix))
            .map(|f| f.contents.as_str())
    }
}

/// A backend renderer — projects the neutral [`ServiceSpec`] onto one init system.
/// Pure: no root, no IO. The privileged side (write files, run the verbs) is a
/// separate driver, exactly like [`crate::systemd::install_systemd`] sits behind
/// the pure [`crate::systemd::systemd_unit`].
pub trait ServiceRenderer {
    fn backend(&self) -> Backend;
    /// Render `spec`, or REFUSE it. A backend that cannot honour something the spec
    /// declares (a secret with no private channel, a value that would corrupt the
    /// output) returns an error rather than quietly rendering something else.
    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService>;
}

/// Resolve a [`Backend`] to its renderer. `Some` for the implemented backends,
/// `None` for the declared-but-unrendered ones — honest, never a fake success.
pub fn renderer_for(backend: Backend) -> Option<Box<dyn ServiceRenderer>> {
    match backend {
        Backend::SystemdSystem => Some(Box::new(SystemdRenderer {
            scope: Scope::System,
        })),
        Backend::SystemdUser => Some(Box::new(SystemdRenderer { scope: Scope::User })),
        Backend::OpenRc => Some(Box::new(OpenRcRenderer)),
        Backend::SysV => Some(Box::new(SysVRenderer)),
        Backend::Runit => Some(Box::new(RunitRenderer)),
        Backend::S6Rc => Some(Box::new(S6RcRenderer)),
        Backend::Dinit => Some(Box::new(DinitRenderer)),
        // Kubernetes and the Windows SCM are rendered by `skidbladnir` (its
        // `service::renderer_for`): they are not a systemd host's concern.
        Backend::Kubernetes => None,
        Backend::Oci => Some(Box::new(OciRenderer)),
        Backend::Quadlet => Some(Box::new(QuadletRenderer)),
        Backend::WindowsService => None,
    }
}

// ───────────────────────────── systemd renderer ─────────────────────────────

/// systemd renderer — bridges the neutral [`ServiceSpec`] to the existing,
/// tested [`crate::systemd::systemd_unit_scoped`]. The tree REUSES the install
/// limb that already ships rather than forking a second systemd code path.
pub struct SystemdRenderer {
    pub scope: Scope,
}

impl ServiceRenderer for SystemdRenderer {
    fn backend(&self) -> Backend {
        match self.scope {
            Scope::User => Backend::SystemdUser,
            Scope::System => Backend::SystemdSystem,
        }
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let sys = to_systemd_spec(spec);
        // The same refusal the install applies — a preview must not hand out a unit
        // the install would reject.
        sys.validate().map_err(|e| anyhow::anyhow!(e))?;
        let unit = crate::systemd::systemd_unit_scoped(&sys, self.scope);
        let unit_path = match self.scope {
            Scope::System => format!("/etc/systemd/system/{}.service", spec.name),
            // user scope: relative to $HOME (the driver joins it).
            Scope::User => format!(".config/systemd/user/{}.service", spec.name),
        };
        let user_flag: &[&str] = if matches!(self.scope, Scope::User) {
            &["--user"]
        } else {
            &[]
        };
        let argv = |verb: &str| -> Vec<String> {
            let mut v: Vec<String> = vec!["systemctl".into()];
            v.extend(user_flag.iter().map(|s| s.to_string()));
            v.push(verb.into());
            v.push(format!("{}.service", spec.name));
            v
        };
        Ok(RenderedService {
            backend: self.backend(),
            files: std::iter::once(RenderedFile {
                path: unit_path,
                contents: unit,
                mode: 0o644,
            })
            // The private env file, at the ONE location the unit names.
            .chain(
                crate::systemd::env_file_location(&sys, self.scope).map(|loc| RenderedFile {
                    path: loc.rendered_path(),
                    contents: crate::systemd::env_file(&sys),
                    mode: 0o600,
                }),
            )
            .chain(config_rendered_files(spec))
            .collect(),
            enable: argv("enable"),
            start: argv("start"),
            status: argv("is-active"),
            stop: argv("stop"),
            remove: argv("disable"),
        })
    }
}

// ───────────────────────────── OpenRC renderer ──────────────────────────────

/// OpenRC renderer — emits an `#!/sbin/openrc-run` service script for
/// `/etc/init.d/<name>` plus, when the spec carries env, an `/etc/conf.d/<name>`
/// file (OpenRC auto-sources it). Uses `supervise-daemon` for real restart
/// supervision (the modern OpenRC equivalent of `Restart=`), `command_user` for
/// the run-as user, and a `depend()` block for ordering/needs.
pub struct OpenRcRenderer;

impl ServiceRenderer for OpenRcRenderer {
    fn backend(&self) -> Backend {
        Backend::OpenRc
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let mut s = String::new();
        s.push_str("#!/sbin/openrc-run\n");
        // openrc-run `eval`s command, command_args and directory into the
        // start-stop-daemon/supervise-daemon line, so those carry shell words quoted
        // TWICE: once for the assignment, once for the eval (stinky #10).
        s.push_str(&format!("description={}\n", sh_quote(&spec.description)));
        s.push_str(&format!("command={}\n", sh_quote(&sh_quote(&spec.exec))));
        if !spec.args.is_empty() {
            s.push_str(&format!(
                "command_args={}\n",
                sh_quote(&sh_words(spec.args.iter().map(String::as_str)))
            ));
        }
        // user scope runs as the caller → no command_user.
        if !matches!(spec.scope, Scope::User) {
            if let Some(u) = &spec.user {
                s.push_str(&format!("command_user=\"{u}\"\n"));
            }
            // OpenRC has NO supplementary-group knob. `command_user` is passed to
            // `start-stop-daemon --user`, which takes one `user[:group]` pair —
            // there is no second axis to render `SupplementaryGroups=` onto, and
            // inventing one (a `start_pre()` that runs `usermod`) would be both a
            // shell-out and a host mutation this renderer has no business making.
            //
            // So it is declared UNHONOURED, in the script, where the operator
            // reading it will see it — rather than dropped in silence, which would
            // hand out a service that looks correctly rendered and cannot open the
            // resource it declared it needs. Same discipline as draupnir refusing to
            // report EACCES as ENOENT: a gap named beats a gap hidden.
            if !spec.supplementary_groups.is_empty() {
                s.push_str(&format!(
                    "# skidbladnir: NOT HONOURED — SupplementaryGroups=\"{}\" was declared, and\n\
                     # OpenRC has no equivalent (start-stop-daemon takes one user[:group] only).\n\
                     # Grant it on the host, e.g. by adding the run-as account to those groups.\n",
                    spec.supplementary_groups.join(" ")
                ));
            }
        }
        // THE RESOURCE CEILING. OpenRC has a REAL route to it — `rc_cgroup_settings`,
        // which openrc's cgroup support writes verbatim into the service's own cgroup
        // directory, one `<file> <value>` pair per line. So `memory.high` and
        // `memory.max` are set on the same two cgroup-v2 files systemd's
        // `MemoryHigh=`/`MemoryMax=` write, and this is honoured rather than noted.
        //
        // With ONE precondition that is not ours to satisfy: openrc only applies these
        // when it is driving cgroups v2 (`rc_cgroup_mode="unified"`, OpenRC ≥ 0.31 with
        // a cgroup-v2 host). It is set here as well as globally in `/etc/rc.conf`,
        // because a script that carries the settings and not the mode would be a
        // ceiling that silently does nothing. The precondition is stated in the script
        // too: a bound whose effect depends on the host must SAY so where the operator
        // reads it, not be assumed away. (Rendered from dwarf, which runs systemd —
        // this path is not verified against a live OpenRC host, and the note says which
        // half is the host's job.)
        //
        // Outside the user-scope branch above on purpose: a cgroup ceiling is not a
        // privilege directive, and OpenRC has no per-user scope for it to be invalid in.
        if let Some(m) = &spec.memory {
            s.push_str(
                "# skidbladnir: RESOURCE CEILING via OpenRC's own cgroups-v2 support.\n\
                 # REQUIRES a cgroup-v2 host with unified mode (OpenRC >= 0.31); on a host\n\
                 # where openrc is not driving cgroups v2 these settings are IGNORED and this\n\
                 # service is UNBOUNDED — verify with: cat /sys/fs/cgroup/<svc>/memory.max\n",
            );
            s.push_str("rc_cgroup_mode=\"unified\"\n");
            s.push_str("rc_cgroup_settings=\"\n");
            if let Some(b) = m.soft_bytes() {
                s.push_str(&format!("memory.high {b}\n"));
            }
            if let Some(b) = m.hard_bytes() {
                s.push_str(&format!("memory.max {b}\n"));
            }
            s.push_str("\"\n");
        }
        // …and THE REACTION, which OpenRC canNOT honour — and this is the one backend
        // where saying so takes an argument, because it looks like it can.
        //
        // `rc_cgroup_settings` writes arbitrary cgroup-v2 attributes, and systemd's own
        // `OOMPolicy=kill` is DEFINED as "setting the memory.oom.group attribute to 1"
        // (man systemd.service). So a third of the axis has an exact route here. The
        // other two thirds have none: `continue` and `stop` are reactions of the
        // service MANAGER to a kill event, and `supervise-daemon` has no notion of an
        // oom-kill failed state — it cannot tell an OOM kill from any other death, so
        // it can neither keep a unit running past one nor stop the rest of it.
        //
        // Rendering only the third that is reachable would turn "this backend has no
        // OOM policy" into "this backend has an OOM policy that silently means
        // something else for two of its three values" — the exact shape of defect this
        // axis exists to end. So the note NAMES memory.oom.group as the by-hand route
        // and claims nothing. (Also unverified against a live OpenRC host — dwarf and
        // t14s are both systemd — which is a second reason not to render it.)
        if let Some(p) = spec.oom_policy {
            s.push_str(&oom_not_honoured_note(
                "# ",
                "OpenRC (supervise-daemon)",
                &[
                    "One THIRD of this axis has an exact route here and is still not",
                    "rendered: OOMPolicy=kill is defined as setting cgroup-v2",
                    "memory.oom.group=1, which rc_cgroup_settings can write. `continue`",
                    "and `stop` are service-MANAGER reactions to a kill event, and",
                    "supervise-daemon has no oom-kill failed state to react with — so",
                    "rendering only the reachable third would make the other two mean",
                    "something else in silence. To grant `kill` by hand, add",
                    "  memory.oom.group 1",
                    "to rc_cgroup_settings above (needs rc_cgroup_mode=\"unified\").",
                ],
                p,
            ));
        }
        // …and THE RATE LIMIT. supervise-daemon does have respawn accounting beside
        // the `respawn_delay` this renderer already emits, but the variable NAMES could
        // not be checked from here (no openrc, no supervise-daemon, no man page on this
        // box) — and an unrecognised variable in a conf.d file is silently ignored by
        // OpenRC, which would render a limit that looks granted and does nothing. That
        // is strictly worse than the note, so the note names the candidate and marks it
        // unverified.
        if let Some(l) = spec.start_limit {
            s.push_str(&start_limit_not_honoured_note(
                "# ",
                "OpenRC (supervise-daemon)",
                &[
                    "UNVERIFIED CANDIDATE: supervise-daemon is understood to accept",
                    "`respawn_max` and `respawn_period` alongside the `respawn_delay`",
                    "this script already sets. Neither name could be checked against a",
                    "live host from the box that generated this file, and OpenRC ignores",
                    "an unrecognised variable in silence — so confirm both against your",
                    "supervise-daemon(8) before relying on them.",
                ],
                l,
                spec.restart_sec,
            ));
        }
        if let Some(a) = spec.oom_score_adjust {
            s.push_str(&oom_score_not_honoured_note(
                "# ",
                "OpenRC (supervise-daemon)",
                a,
            ));
        }
        if let Some(d) = &spec.working_dir {
            s.push_str(&format!("directory={}\n", sh_quote(&sh_quote(d))));
        }
        s.push_str("command_background=true\n");
        s.push_str(&format!("pidfile=\"/run/{}.pid\"\n", spec.name));
        // Restart supervision: supervise-daemon respawns; backoff = restart_sec.
        // RestartPolicy::No ⇒ no supervisor (a plain foreground command).
        if !matches!(spec.restart, RestartPolicy::No) {
            s.push_str("supervisor=\"supervise-daemon\"\n");
            s.push_str(&format!("respawn_delay={}\n", spec.restart_sec));
        }
        // depend(): `need net` is the OpenRC twin of network-online.target (always
        // on, matching the systemd renderer); then `need`/`after` from the spec.
        s.push_str("\ndepend() {\n");
        s.push_str("\tneed net\n");
        for dep in &spec.needs {
            s.push_str(&format!("\tneed {}\n", sh_quote(dep)));
        }
        for dep in &spec.after {
            s.push_str(&format!("\tafter {}\n", sh_quote(dep)));
        }
        s.push_str("}\n");

        let mut files = vec![RenderedFile {
            path: format!("/etc/init.d/{}", spec.name),
            contents: s,
            mode: 0o755,
        }];
        // Env → /etc/conf.d/<name> (auto-sourced by OpenRC). mode 0600 (it may hold
        // secrets), matching the systemd env-file posture.
        if !spec.file_env().is_empty() {
            let mut conf = String::new();
            for (k, v) in spec.file_env() {
                conf.push_str(&format!("export {k}={}\n", sh_quote(v)));
            }
            files.push(RenderedFile {
                path: format!("/etc/conf.d/{}", spec.name),
                contents: conf,
                mode: 0o600,
            });
        }

        let n = &spec.name;
        Ok(RenderedService {
            backend: Backend::OpenRc,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            // enable at boot = add to the `default` runlevel.
            enable: vec![
                "rc-update".into(),
                "add".into(),
                n.clone(),
                "default".into(),
            ],
            start: vec!["rc-service".into(), n.clone(), "start".into()],
            status: vec!["rc-service".into(), n.clone(), "status".into()],
            stop: vec!["rc-service".into(), n.clone(), "stop".into()],
            remove: vec!["rc-update".into(), "del".into(), n.clone(), "default".into()],
        })
    }
}

// ────────────────────────────── SysV renderer ───────────────────────────────

/// SysV-init renderer — the **universal fallback** for any target that is not
/// systemd / OpenRC / k8s. Emits an LSB `/etc/init.d/<name>` script (with a
/// proper `### BEGIN INIT INFO` header for `insserv`/`update-rc.d` ordering) that
/// drives the daemon through `start-stop-daemon`, plus, when the spec carries
/// env, an `/etc/default/<name>` file the script sources. Pure: no root, no IO —
/// the privileged driver writes the files and runs the verbs.
///
/// Note: classic SysV init does not itself supervise/respawn a service, so
/// [`RestartPolicy`] can only shape the stop retry cadence here, not on-crash
/// restart — that is a documented limitation of the fallback, not a fake.
pub struct SysVRenderer;

impl ServiceRenderer for SysVRenderer {
    fn backend(&self) -> Backend {
        Backend::SysV
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        // Required-Start deps: always network + local fs, then the spec's needs
        // (LSB virtual facilities are `$`-prefixed; a plain service name is used
        // verbatim, matching how insserv resolves other init scripts).
        let mut required = String::from("$network $local_fs $remote_fs");
        for dep in &spec.needs {
            required.push(' ');
            required.push_str(dep);
        }
        // `after` has no first-class SysV notion beyond ordering; fold it into
        // Required-Start too so insserv orders this script after those.
        for dep in &spec.after {
            required.push(' ');
            required.push_str(dep);
        }

        let mut s = String::new();
        s.push_str("#!/bin/sh\n");
        s.push_str("### BEGIN INIT INFO\n");
        s.push_str(&format!("# Provides:          {n}\n"));
        s.push_str(&format!("# Required-Start:    {required}\n"));
        s.push_str("# Required-Stop:     $network $local_fs $remote_fs\n");
        s.push_str("# Default-Start:     2 3 4 5\n");
        s.push_str("# Default-Stop:      0 1 6\n");
        s.push_str(&format!("# Short-Description: {}\n", spec.description));
        s.push_str("### END INIT INFO\n\n");

        s.push_str(&format!("NAME={n}\n"));
        s.push_str(&format!("DAEMON={}\n", sh_quote(&spec.exec)));
        // Shell words, `eval`ed into "$@" at start — so an operator override in
        // /etc/default keeps working AND an arg with a space stays one arg.
        s.push_str(&format!(
            "DAEMON_ARGS={}\n",
            sh_quote(&sh_words(spec.args.iter().map(String::as_str)))
        ));
        s.push_str(&format!("PIDFILE=\"/run/{n}.pid\"\n"));
        // Source the env file if present (also lets an operator override DAEMON_ARGS).
        s.push_str(&format!(
            "[ -r /etc/default/{n} ] && . /etc/default/{n}\n\n"
        ));
        // THE RESOURCE CEILING — sysvinit has none. `start-stop-daemon` has no cgroup
        // knob and `ulimit -v` in the script is an rlimit, a different mechanism (see
        // `RLIMIT_IS_NOT_A_CGROUP_CEILING`). Declared UNHONOURED in the script, naming
        // both numbers, rather than dropped in silence — the same choice OpenRC makes
        // for supplementary groups.
        if let Some(m) = &spec.memory {
            s.push_str(&memory_not_honoured_note(
                "# ",
                "sysvinit (start-stop-daemon)",
                RLIMIT_IS_NOT_A_CGROUP_CEILING,
                m,
            ));
            s.push('\n');
        }
        // …and THE REACTION, which SysV cannot honour either, for a reason that is
        // not the same one: the ceiling has a near-miss here (an rlimit) and the
        // reaction has none at all — `start-stop-daemon` never learns that the kernel
        // reaped anything.
        if let Some(p) = spec.oom_policy {
            s.push_str(&oom_not_honoured_note(
                "# ",
                "sysvinit (start-stop-daemon)",
                NO_CGROUP_OOM_INTEGRATION,
                p,
            ));
            s.push('\n');
        }
        // …and THE RATE LIMIT, which SysV cannot honour for the most basic reason of
        // the five: classic init does not supervise or respawn the service AT ALL, so
        // there is no restart loop to rate-limit. The declaration is about a behaviour
        // this backend does not have.
        if let Some(l) = spec.start_limit {
            s.push_str(&start_limit_not_honoured_note(
                "# ",
                "sysvinit (start-stop-daemon)",
                &[
                    "There is no candidate knob here at all: classic init does not",
                    "supervise or respawn the service, so nothing counts restarts and",
                    "nothing could give up. Whatever restarts this service — a cron",
                    "watchdog, a human — owns the limit, and this note is where the",
                    "declared intent can be read off.",
                ],
                l,
                spec.restart_sec,
            ));
            s.push('\n');
        }
        if let Some(a) = spec.oom_score_adjust {
            s.push_str(&oom_score_not_honoured_note(
                "# ",
                "sysvinit (start-stop-daemon)",
                a,
            ));
        }

        // --chuid honours the run-as user (and group when set); user scope runs as
        // the caller so no chuid is emitted.
        let chuid = if matches!(spec.scope, Scope::User) {
            String::new()
        } else if let Some(u) = &spec.user {
            match &spec.group {
                Some(g) => format!(" --chuid {u}:{g}"),
                None => format!(" --chuid {u}"),
            }
        } else {
            String::new()
        };
        let chdir = spec
            .working_dir
            .as_deref()
            .map(|d| format!(" --chdir {}", sh_quote(d)))
            .unwrap_or_default();
        // Stop retry cadence: give the daemon restart_sec seconds to exit on TERM
        // before KILL (RestartPolicy::No keeps the default short retry).
        let retry = if matches!(spec.restart, RestartPolicy::No) {
            "TERM/5/KILL".to_string()
        } else {
            format!("TERM/{}/KILL", spec.restart_sec.max(1))
        };

        s.push_str("case \"$1\" in\n");
        s.push_str("  start)\n");
        s.push_str(&format!(
            "    eval \"set -- $DAEMON_ARGS\"\n    start-stop-daemon --start --quiet --background --make-pidfile \\\n      --pidfile \"$PIDFILE\"{chuid}{chdir} --exec \"$DAEMON\" -- \"$@\"\n"
        ));
        s.push_str("    ;;\n");
        s.push_str("  stop)\n");
        s.push_str(&format!(
            "    start-stop-daemon --stop --quiet --pidfile \"$PIDFILE\" --retry {retry}\n"
        ));
        s.push_str("    rm -f \"$PIDFILE\"\n");
        s.push_str("    ;;\n");
        s.push_str("  status)\n");
        s.push_str("    start-stop-daemon --status --pidfile \"$PIDFILE\"\n");
        s.push_str("    exit $?\n");
        s.push_str("    ;;\n");
        s.push_str("  restart|force-reload)\n");
        s.push_str("    \"$0\" stop\n");
        s.push_str("    \"$0\" start\n");
        s.push_str("    ;;\n");
        s.push_str("  *)\n");
        s.push_str("    echo \"Usage: $0 {start|stop|status|restart}\" >&2\n");
        s.push_str("    exit 2\n");
        s.push_str("    ;;\n");
        s.push_str("esac\n");

        let mut files = vec![RenderedFile {
            path: format!("/etc/init.d/{n}"),
            contents: s,
            mode: 0o755,
        }];
        // Env → /etc/default/<name> (sourced by the script). mode 0600 — it may
        // hold secrets, matching the systemd/OpenRC env-file posture.
        if !spec.file_env().is_empty() {
            let mut conf = String::new();
            for (k, v) in spec.file_env() {
                conf.push_str(&format!("{k}={}\n", sh_quote(v)));
            }
            files.push(RenderedFile {
                path: format!("/etc/default/{n}"),
                contents: conf,
                mode: 0o600,
            });
        }

        let script = format!("/etc/init.d/{n}");
        Ok(RenderedService {
            backend: Backend::SysV,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            // enable at boot = register the LSB script's runlevel links.
            enable: vec!["update-rc.d".into(), n.clone(), "defaults".into()],
            // Drive start/stop/status through the script itself (guaranteed to
            // exist) rather than assuming a `service`/`invoke-rc.d` wrapper.
            start: vec![script.clone(), "start".into()],
            status: vec![script.clone(), "status".into()],
            stop: vec![script, "stop".into()],
            remove: vec!["update-rc.d".into(), "-f".into(), n.clone(), "remove".into()],
        })
    }
}

// ────────────────────────────── runit renderer ──────────────────────────────

/// runit renderer — emits a runit **service directory** `/etc/sv/<name>/` (the
/// native shape on Void, Artix, Alpine-with-runit, …): an executable `run` script
/// that `exec`s the daemon under `runsv` supervision, an optional `finish` script
/// that encodes the restart policy, an optional mode-0600 `conf` env file the
/// `run` script sources, and (system scope) a `log/run` svlogd logger. Pure: no
/// root, no IO — the privileged driver writes the dir + runs the verbs.
///
/// Honest mappings (runit's model differs from systemd's, so we say where):
/// - **Restart.** `runsv` respawns the service on *any* exit and has no exit-code
///   awareness, so [`RestartPolicy::OnFailure`] and [`RestartPolicy::Always`] both
///   map to "supervised + a `restart_sec` backoff `sleep` in `finish`". Only
///   [`RestartPolicy::No`] is distinct: `finish` runs `sv down .` so the service is
///   taken down after it first exits rather than respawned.
/// - **Ordering.** runit's only dependency primitive is a `sv check <dep>` guard at
///   the top of `run` (runsv retries `run` until the dep is up); both `needs` and
///   `after` become such a guard.
/// - **User scope.** Files land home-relative under `.local/sv/<name>` (the driver
///   joins `$HOME`), mirroring the systemd `--user` renderer's `.config/…` posture;
///   the daemon runs as the caller (no `chpst -u`) and the root-owned `log/run`
///   svlogd logger is omitted.
pub struct RunitRenderer;

impl ServiceRenderer for RunitRenderer {
    fn backend(&self) -> Backend {
        Backend::Runit
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        let user_scope = matches!(spec.scope, Scope::User);
        // Service dir + scan-dir symlink. System scope: absolute `/etc/sv/<name>`
        // supervised from `/var/service/<name>` (the Void/Artix runsvdir default).
        // User scope: home-relative (the driver joins `$HOME`), like the systemd
        // `--user` renderer's `.config/…` unit path.
        let sv_dir = if user_scope {
            format!(".local/sv/{n}")
        } else {
            format!("/etc/sv/{n}")
        };
        let scan_link = if user_scope {
            format!(".local/service/{n}")
        } else {
            format!("/var/service/{n}")
        };

        // ── run: the supervised process ──
        let mut run = String::new();
        run.push_str("#!/bin/sh\n");
        run.push_str(&format!("# runit service for {n} — supervised by runsv.\n"));
        run.push_str("exec 2>&1\n");
        // Source the env file if present (also lets an operator override vars).
        run.push_str("[ -r ./conf ] && . ./conf\n");
        // THE RESOURCE CEILING — runit has none. `chpst -m` is an rlimit
        // (RLIMIT_DATA/RLIMIT_AS on the child), not a cgroup ceiling; see
        // `RLIMIT_IS_NOT_A_CGROUP_CEILING`. Named in the `run` script, with both
        // numbers, rather than dropped in silence.
        if let Some(m) = &spec.memory {
            run.push_str(&memory_not_honoured_note(
                "# ",
                "runit (runsv/chpst)",
                RLIMIT_IS_NOT_A_CGROUP_CEILING,
                m,
            ));
        }
        // …and THE REACTION. `runsv` restarts `run` whenever it exits, which looks
        // like a policy and is not one: it is the restart axis, and it cannot tell an
        // OOM kill from a clean exit — so `continue` (survive the kill) is not
        // expressible at all.
        if let Some(p) = spec.oom_policy {
            run.push_str(&oom_not_honoured_note(
                "# ",
                "runit (runsv/chpst)",
                NO_CGROUP_OOM_INTEGRATION,
                p,
            ));
        }
        // …and THE RATE LIMIT. `runsv` restarts `run` unconditionally, forever, and
        // has no counter and no give-up state — the closest thing to a limit is the
        // `sleep` backoff this renderer already writes, which slows the loop down
        // without ever ending it.
        if let Some(l) = spec.start_limit {
            run.push_str(&start_limit_not_honoured_note(
                "# ",
                "runit (runsv/chpst)",
                &[
                    "No candidate: runsv respawns `run` forever with no attempt counter",
                    "and no failed state to land in. The `sleep` backoff above slows the",
                    "loop; it cannot end it. A `./finish` script that counts its own",
                    "invocations and calls `sv down .` is the by-hand route.",
                ],
                l,
                spec.restart_sec,
            ));
        }
        if let Some(a) = spec.oom_score_adjust {
            run.push_str(&oom_score_not_honoured_note("# ", "runit (runsv/chpst)", a));
        }
        // Dependency guards — runit's only ordering primitive: keep exiting until
        // each dep is up (runsv retries `run`). `needs` and `after` both map here.
        for dep in &spec.needs {
            run.push_str(&format!("sv check {} >/dev/null || exit 1\n", sh_quote(dep)));
        }
        for dep in &spec.after {
            run.push_str(&format!("sv check {} >/dev/null || exit 1\n", sh_quote(dep)));
        }
        if let Some(d) = &spec.working_dir {
            run.push_str(&format!("cd {} || exit 1\n", sh_quote(d)));
        }
        // exec the daemon. System scope drops privileges via `chpst -u user[:group]`;
        // user scope runs as the caller (no `chpst -u`).
        let chpst = if user_scope {
            String::new()
        } else if let Some(u) = &spec.user {
            match &spec.group {
                Some(g) => format!("chpst -u {u}:{g} "),
                None => format!("chpst -u {u} "),
            }
        } else {
            String::new()
        };
        run.push_str(&format!("exec {chpst}{}\n", spec.command_line()));

        let mut files = vec![RenderedFile {
            path: format!("{sv_dir}/run"),
            contents: run,
            mode: 0o755,
        }];

        // ── finish: encodes the restart policy ──
        // No ⇒ take the service down after it exits (runit otherwise always
        // respawns). On-failure/Always ⇒ a `restart_sec` backoff before respawn.
        let finish = if matches!(spec.restart, RestartPolicy::No) {
            Some("#!/bin/sh\n# RestartPolicy::No — do not respawn.\nsv down .\n".to_string())
        } else if spec.restart_sec > 0 {
            Some(format!(
                "#!/bin/sh\n# Restart backoff before runsv respawns.\nexec sleep {}\n",
                spec.restart_sec
            ))
        } else {
            None
        };
        if let Some(f) = finish {
            files.push(RenderedFile {
                path: format!("{sv_dir}/finish"),
                contents: f,
                mode: 0o755,
            });
        }

        // ── conf: env sourced by `run` (mode 0600 — may hold secrets, matching the
        // systemd/OpenRC/SysV env-file posture) ──
        if !spec.file_env().is_empty() {
            let mut conf = String::new();
            for (k, v) in spec.file_env() {
                conf.push_str(&format!("export {k}={}\n", sh_quote(v)));
            }
            files.push(RenderedFile {
                path: format!("{sv_dir}/conf"),
                contents: conf,
                mode: 0o600,
            });
        }

        // ── log/run: an svlogd logger (system scope only — `/var/log` is root-owned;
        // a user service just lets runsv discard stdout) ──
        if !user_scope {
            files.push(RenderedFile {
                path: format!("{sv_dir}/log/run"),
                contents: format!("#!/bin/sh\nexec svlogd -tt /var/log/{n}\n"),
                mode: 0o755,
            });
        }

        Ok(RenderedService {
            backend: Backend::Runit,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            // enable at boot = symlink the service dir into the runsvdir scan dir;
            // runsvdir then supervises AND auto-starts it.
            enable: vec!["ln".into(), "-s".into(), sv_dir, scan_link.clone()],
            start: vec!["sv".into(), "up".into(), scan_link.clone()],
            status: vec!["sv".into(), "status".into(), scan_link.clone()],
            stop: vec!["sv".into(), "down".into(), scan_link.clone()],
            // un-enable = drop the scan link; runsvdir then stops supervising it.
            remove: vec!["rm".into(), "-f".into(), scan_link],
        })
    }
}

// ────────────────────────────── dinit renderer ──────────────────────────────

/// dinit renderer — emits a **textual service description** for `/etc/dinit.d/<name>`
/// (the native shape on Chimera Linux, Artix-dinit, …): a `key = value` file dinit
/// loads directly, plus, when the spec carries env, a mode-0600 `env-file` the
/// description points at. Pure: no root, no IO — the privileged driver writes the
/// file(s) and runs the `dinitctl` verbs.
///
/// The mapping onto dinit's model (stated where it differs from systemd):
/// - **Command.** `type = process` + `command = <exec + args>` (dinit splits the
///   command line itself: each word is double-quoted with `\\`/`"` escaped and `$`
///   doubled, so dinit hands the process exactly `[exec, args…]`).
/// - **Restart.** dinit's `restart = yes` respawns on *any* unexpected exit with no
///   exit-code awareness, so [`RestartPolicy::OnFailure`] and [`RestartPolicy::Always`]
///   both map to `restart = yes` + `restart-delay = <restart_sec>` (same honest limit
///   as the runit backend). [`RestartPolicy::No`] ⇒ `restart = no`.
/// - **Dependencies.** `needs` → `depends-on` (a hard dependency dinit pulls in and
///   orders before this service); `after` → `after` (pure ordering, no pull-in). Like
///   the runit renderer, no synthetic network dependency is injected — dinit's
///   network service name is distro-specific, and naming a missing service would fail
///   the load; a consumer that needs it declares it explicitly.
/// - **User.** system scope emits `run-as = <user>` (dinit runs the process as that
///   user and its primary group); dinit has no separate group directive, so
///   [`ServiceSpec::group`] is not expressible here (documented, not faked). User
///   scope runs as the caller — no `run-as`.
/// - **Scope.** system: `/etc/dinit.d/<name>` (+ `run-as`, a `/var/log/<name>.log`
///   `logfile`) driven by `dinitctl`. User: home-relative `.config/dinit.d/<name>`
///   (the driver joins `$HOME`, mirroring the systemd `--user` renderer), no `run-as`,
///   no root logfile, driven by `dinitctl --user`.
pub struct DinitRenderer;

impl ServiceRenderer for DinitRenderer {
    fn backend(&self) -> Backend {
        Backend::Dinit
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        let user_scope = matches!(spec.scope, Scope::User);
        // Description file + (env-file) live under the dinit config dir. System:
        // /etc/dinit.d/<name>; user: home-relative .config/dinit.d/<name> (the driver
        // joins $HOME, like the systemd --user renderer's .config/… path).
        let (desc_path, env_path) = if user_scope {
            (
                format!(".config/dinit.d/{n}"),
                format!(".config/dinit.d/config/{n}.env"),
            )
        } else {
            (
                format!("/etc/dinit.d/{n}"),
                format!("/etc/dinit.d/config/{n}.env"),
            )
        };

        let mut d = String::new();
        d.push_str(&format!("# dinit service description for {n}.\n"));
        d.push_str("type = process\n");
        // THE RESOURCE CEILING — dinit has none. Its `rlimit-*` settings
        // (`rlimit-data`, `rlimit-nofile`, …) are POSIX rlimits, a different mechanism
        // from a cgroup ceiling (see `RLIMIT_IS_NOT_A_CGROUP_CEILING`). Named in the
        // service description with both numbers rather than dropped in silence.
        if let Some(m) = &spec.memory {
            d.push_str(&memory_not_honoured_note(
                "# ",
                "dinit",
                RLIMIT_IS_NOT_A_CGROUP_CEILING,
                m,
            ));
        }
        // …and THE REACTION, which dinit has no notion of: it observes the process
        // exiting, never WHY, so an OOM kill is indistinguishable from any other
        // signal death.
        if let Some(p) = spec.oom_policy {
            d.push_str(&oom_not_honoured_note(
                "# ",
                "dinit",
                NO_CGROUP_OOM_INTEGRATION,
                p,
            ));
        }
        // …and THE RATE LIMIT. dinit is the one non-systemd backend believed to have a
        // real structural twin, and it is STILL not rendered, for the OpenRC reason:
        // the property names could not be verified from this box, and a mis-spelled
        // dinit property is a limit that silently does not exist.
        if let Some(l) = spec.start_limit {
            d.push_str(&start_limit_not_honoured_note(
                "# ",
                "dinit",
                &[
                    "UNVERIFIED CANDIDATE: dinit is understood to have a real structural",
                    "twin for this — a restart count within a restart interval, after",
                    "which the service is left stopped. The exact property names could",
                    "not be checked from the box that generated this file, and a",
                    "mis-spelled dinit property is a limit that silently does not exist,",
                    "so nothing is rendered. Confirm against your dinit-service(5).",
                ],
                l,
                spec.restart_sec,
            ));
        }
        if let Some(a) = spec.oom_score_adjust {
            d.push_str(&oom_score_not_honoured_note("# ", "dinit", a));
        }
        let words: Vec<String> = std::iter::once(&spec.exec)
            .chain(&spec.args)
            .map(|w| dinit_word(w))
            .collect();
        d.push_str(&format!("command = {}\n", words.join(" ")));
        if let Some(wd) = &spec.working_dir {
            d.push_str(&format!("working-dir = {}\n", dinit_word(wd)));
        }
        // run-as: system scope drops privileges to the run-as user; user scope runs
        // as the caller (no run-as). dinit has no separate group directive.
        if !user_scope {
            if let Some(u) = &spec.user {
                d.push_str(&format!("run-as = {u}\n"));
            }
        }
        // logfile: capture stdout/stderr to /var/log/<name>.log (system scope only —
        // /var/log is root-owned; a user service lets dinit inherit its console).
        if !user_scope {
            d.push_str(&format!("logfile = /var/log/{n}.log\n"));
        }
        // env → an env-file the description points at (mode 0600 — may hold secrets,
        // matching the systemd/OpenRC/SysV/runit env-file posture).
        if !spec.file_env().is_empty() {
            d.push_str(&format!("env-file = {env_path}\n"));
        }
        // Restart: `no` maps to restart = no; on-failure/always ⇒ restart = yes with a
        // restart_sec backoff (dinit has no exit-code awareness, same as runit).
        if matches!(spec.restart, RestartPolicy::No) {
            d.push_str("restart = no\n");
        } else {
            d.push_str("restart = yes\n");
            d.push_str(&format!("restart-delay = {}.0\n", spec.restart_sec));
        }
        // Dependencies: needs → depends-on (hard, pulled in + ordered before);
        // after → after (pure ordering, no pull-in).
        for dep in &spec.needs {
            d.push_str(&format!("depends-on = {dep}\n"));
        }
        for dep in &spec.after {
            d.push_str(&format!("after = {dep}\n"));
        }

        let mut files = vec![RenderedFile {
            path: desc_path,
            contents: d,
            mode: 0o644,
        }];
        if !spec.file_env().is_empty() {
            let mut env = String::new();
            for (k, v) in spec.file_env() {
                // dinit env-file lines are literal NAME=VALUE (no shell quoting/export).
                env.push_str(&literal_env_line("dinit env-file", k, v)?);
            }
            files.push(RenderedFile {
                path: env_path,
                contents: env,
                mode: 0o600,
            });
        }

        // Verbs via dinitctl (--user for user scope, mirroring the systemd renderer).
        let user_flag: &[&str] = if user_scope { &["--user"] } else { &[] };
        let argv = |verb: &str| -> Vec<String> {
            let mut v: Vec<String> = vec!["dinitctl".into()];
            v.extend(user_flag.iter().map(|s| s.to_string()));
            v.push(verb.into());
            v.push(n.clone());
            v
        };
        Ok(RenderedService {
            backend: Backend::Dinit,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            // enable at boot = link the service into dinit's boot dependencies.
            enable: argv("enable"),
            start: argv("start"),
            status: argv("status"),
            stop: argv("stop"),
            remove: argv("disable"),
        })
    }
}

// ────────────────────────────── s6-rc renderer ──────────────────────────────

/// s6-rc renderer — emits a **compiled-source service-definition directory** for
/// `/etc/s6-rc/source/<name>/` (the native shape on Artix-s6, Obarun, Adélie, … and
/// any skarnet s6-rc stack): the small set of one-purpose files `s6-rc-compile`
/// consumes — `type`, `run`, optional `finish`/`timeout-finish`, a `dependencies.d/`
/// directory, and a recognised `env/` directory (copied verbatim into the live
/// service dir, read back with `s6-envdir`). Pure: no root, no IO — the privileged
/// driver writes the tree, compiles it, and runs the `s6-rc` verbs.
///
/// The mapping onto s6's model (stated where it differs from systemd):
/// - **Type / command.** `type = longrun` (a supervised daemon under
///   `s6-supervise`) plus a `run` script that `exec`s the neutral
///   [`ServiceSpec::command_line`]. The
///   `run` script is plain `#!/bin/sh` but composes s6/execline **chainloaders**
///   (`s6-envdir` → `cd` → `s6-setuidgid` → the daemon) in one `exec`, so the env is
///   applied from the service dir *before* the `cd`, exactly as an execline run would.
/// - **Restart.** s6-supervise respawns a `longrun` on *any* exit with no exit-code
///   awareness, so [`RestartPolicy::OnFailure`] and [`RestartPolicy::Always`] both map
///   to "longrun + a `restart_sec` backoff `sleep` in `finish`" (same honest limit as
///   the runit/dinit backends); a `timeout-finish = 0` file lifts the default 5 s
///   finish-kill so the backoff actually elapses. [`RestartPolicy::No`] ⇒ a `finish`
///   that `exit 125`s — the one exit code that tells `s6-supervise` **not** to restart
///   (it acts as if `s6-svc -O` had been called).
/// - **Dependencies.** s6-rc has a single, *hard* dependency notion (a dep is both a
///   pull-in and an ordering edge), so `needs` and `after` both become an (empty)
///   `dependencies.d/<dep>` file — the current, non-deprecated form. Like the
///   runit/dinit renderers, no synthetic network dep is injected (a consumer that
///   needs one declares it; s6-rc's boot bundle owns base ordering).
/// - **User.** system scope prefixes the exec with `s6-setuidgid <user>` (drops to
///   that account: uid + primary gid + supplementary groups). s6-setuidgid resolves the
///   whole account, so an explicit [`ServiceSpec::group`] is **not** separately
///   expressible here (documented, not faked — same honest limit as dinit). User scope
///   runs as the caller (no `s6-setuidgid`).
/// - **Env.** a recognised `env/<KEY>` file per variable (mode 0600 — may hold secrets,
///   matching the systemd/OpenRC/SysV/runit/dinit env-file posture); the `run` script
///   reads them with `s6-envdir ./env`. s6-rc-compile copies the `env` dir verbatim into
///   the live service dir, so `./env` resolves at runtime.
/// - **Scope.** system: `/etc/s6-rc/source/<name>/` (+ `s6-setuidgid`), enabled into the
///   `default` boot bundle via `s6-rc-bundle-update`, driven by `s6-rc … change`. User:
///   home-relative `.config/s6-rc/source/<name>/` (the driver joins `$HOME`, mirroring
///   the systemd `--user` renderer), no `s6-setuidgid`, a home-relative compiled db +
///   scandir.
pub struct S6RcRenderer;

impl ServiceRenderer for S6RcRenderer {
    fn backend(&self) -> Backend {
        Backend::S6Rc
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        let user_scope = matches!(spec.scope, Scope::User);
        // Source-def dir root. System: /etc/s6-rc/source/<name>; user: home-relative
        // .config/s6-rc/source/<name> (the driver joins $HOME, like the systemd --user
        // renderer's .config/… path).
        let src = if user_scope {
            format!(".config/s6-rc/source/{n}")
        } else {
            format!("/etc/s6-rc/source/{n}")
        };

        // ── type: a supervised daemon ──
        let mut files = vec![RenderedFile {
            path: format!("{src}/type"),
            contents: "longrun\n".to_string(),
            mode: 0o644,
        }];

        // ── run: chainload s6-envdir → cd → s6-setuidgid → the daemon in one exec ──
        // Plain #!/bin/sh, but every stage is an s6/execline chainloader, so `s6-envdir
        // ./env` reads the env dir while cwd is still the (live) service dir, BEFORE the
        // `cd` into the working dir — the correct ordering, expressed without execline.
        let mut prefix = String::new();
        if !spec.file_env().is_empty() {
            prefix.push_str("s6-envdir ./env ");
        }
        if let Some(wd) = &spec.working_dir {
            prefix.push_str(&format!("cd {} ", sh_quote(wd)));
        }
        // system scope drops privileges via s6-setuidgid <user>; user scope runs as the
        // caller (no s6-setuidgid). s6-setuidgid resolves the account → no separate group.
        if !user_scope {
            if let Some(u) = &spec.user {
                prefix.push_str(&format!("s6-setuidgid {u} "));
            }
        }
        let mut run = String::new();
        run.push_str("#!/bin/sh\n");
        run.push_str(&format!(
            "# s6-rc longrun service for {n} — supervised by s6-supervise.\n"
        ));
        run.push_str("exec 2>&1\n");
        // THE RESOURCE CEILING — s6 has none. `s6-softlimit -m` is an rlimit, not a
        // cgroup ceiling (see `RLIMIT_IS_NOT_A_CGROUP_CEILING`), and s6-rc has no
        // cgroup notion at all. Named in the `run` script with both numbers.
        if let Some(m) = &spec.memory {
            run.push_str(&memory_not_honoured_note(
                "# ",
                "s6-rc (s6-supervise/s6-softlimit)",
                RLIMIT_IS_NOT_A_CGROUP_CEILING,
                m,
            ));
        }
        // …and THE REACTION. s6-rc has no cgroup notion at all, so there is not even
        // a cgroup for the kernel to reap as a group — the `kill` third of the axis is
        // as unreachable as the other two.
        if let Some(p) = spec.oom_policy {
            run.push_str(&oom_not_honoured_note(
                "# ",
                "s6-rc (s6-supervise/s6-softlimit)",
                NO_CGROUP_OOM_INTEGRATION,
                p,
            ));
        }
        // …and THE RATE LIMIT. s6-supervise restarts forever like runsv; its only
        // throttle is a fixed internal one-second floor between spawns, which is not a
        // budget and cannot be spent.
        if let Some(l) = spec.start_limit {
            run.push_str(&start_limit_not_honoured_note(
                "# ",
                "s6-rc (s6-supervise/s6-softlimit)",
                &[
                    "No candidate: s6-supervise respawns forever, throttled only by a",
                    "fixed internal floor between spawns — a floor is not a budget and",
                    "cannot run out. A `./finish` script that counts and calls",
                    "`s6-svc -d .` is the by-hand route.",
                ],
                l,
                spec.restart_sec,
            ));
        }
        if let Some(a) = spec.oom_score_adjust {
            run.push_str(&oom_score_not_honoured_note(
                "# ",
                "s6-rc (s6-supervise/s6-softlimit)",
                a,
            ));
        }
        run.push_str(&format!("exec {prefix}{}\n", spec.command_line()));
        files.push(RenderedFile {
            path: format!("{src}/run"),
            contents: run,
            mode: 0o755,
        });

        // ── finish: encodes the restart policy ──
        // No ⇒ exit 125 (s6-supervise does not restart; acts as s6-svc -O). On-failure/
        // Always ⇒ a restart_sec backoff sleep before respawn, plus timeout-finish = 0 so
        // s6-supervise's default 5 s finish-kill does not truncate the backoff.
        if matches!(spec.restart, RestartPolicy::No) {
            files.push(RenderedFile {
                path: format!("{src}/finish"),
                contents: "#!/bin/sh\n# RestartPolicy::No — this code tells s6-supervise not to restart.\nexit 125\n".to_string(),
                mode: 0o755,
            });
        } else if spec.restart_sec > 0 {
            files.push(RenderedFile {
                path: format!("{src}/finish"),
                contents: format!(
                    "#!/bin/sh\n# Restart backoff before s6-supervise respawns.\nexec sleep {}\n",
                    spec.restart_sec
                ),
                mode: 0o755,
            });
            // Lift the 5 s default finish timeout so the backoff sleep completes.
            files.push(RenderedFile {
                path: format!("{src}/timeout-finish"),
                contents: "0\n".to_string(),
                mode: 0o644,
            });
        }

        // ── dependencies.d/<dep>: one empty file per dependency (the current,
        // non-deprecated form). s6-rc has a single hard dependency notion, so both
        // `needs` and `after` land here (deduped, order-preserving). ──
        let mut deps: Vec<&String> = Vec::new();
        for dep in spec.needs.iter().chain(spec.after.iter()) {
            if !deps.contains(&dep) {
                deps.push(dep);
            }
        }
        for dep in deps {
            files.push(RenderedFile {
                path: format!("{src}/dependencies.d/{dep}"),
                contents: String::new(),
                mode: 0o644,
            });
        }

        // ── env/<KEY>: one file per variable (mode 0600 — may hold secrets), read at
        // runtime by `s6-envdir ./env`. s6-rc-compile copies the dir into the live
        // service dir verbatim. ──
        //
        // s6-envdir reads the FIRST line, chomps trailing blanks and turns NUL into a
        // newline — so a newline is written as NUL, and a value with a real NUL or a
        // trailing space/tab cannot round-trip and is refused (stinky #10).
        for (k, v) in spec.file_env() {
            if v.contains('\0') || v.ends_with([' ', '\t']) {
                anyhow::bail!(
                    "s6 envdir: env `{k}` holds a NUL or ends in a space/tab, which s6-envdir cannot carry"
                );
            }
            files.push(RenderedFile {
                path: format!("{src}/env/{k}"),
                contents: format!("{}\n", v.replace('\n', "\0")),
                mode: 0o600,
            });
        }

        // Verbs: enable = add to the `default` boot bundle (s6-rc-bundle-update);
        // start/stop = bring the service up/down in the live db (s6-rc change);
        // status = s6-svstat on the live service dir. User scope points the compiled
        // db + scandir home-relative.
        let (compiled, scandir) = if user_scope {
            (
                ".config/s6/rc/compiled".to_string(),
                format!(".local/service/{n}"),
            )
        } else {
            (
                "/etc/s6/rc/compiled".to_string(),
                format!("/run/service/{n}"),
            )
        };
        Ok(RenderedService {
            backend: Backend::S6Rc,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            enable: vec![
                "s6-rc-bundle-update".into(),
                "-c".into(),
                compiled.clone(),
                "add".into(),
                "default".into(),
                n.clone(),
            ],
            start: vec!["s6-rc".into(), "-u".into(), "change".into(), n.clone()],
            status: vec!["s6-svstat".into(), scandir],
            stop: vec!["s6-rc".into(), "-d".into(), "change".into(), n.clone()],
            remove: vec![
                "s6-rc-bundle-update".into(),
                "-c".into(),
                compiled,
                "delete".into(),
                "default".into(),
                n.clone(),
            ],
        })
    }
}

// ─────────────────────────────── OCI renderer ───────────────────────────────

/// The container image reference for a spec's container render (shared by the OCI
/// and Quadlet backends, L5): the explicit [`ServiceSpec::image`] verbatim, else
/// the honest `<name>:latest` placeholder (the same fallback the [`crate::k8s`]
/// renderer uses). ONE image resolution the two container backends consume, never
/// two drifting copies.
fn container_image(spec: &ServiceSpec) -> String {
    spec.image
        .clone()
        .unwrap_or_else(|| format!("{}:latest", spec.name))
}

/// OCI / container renderer — projects the neutral [`ServiceSpec`] onto a
/// **`podman`-style container invocation** (the constellation's one container
/// engine; see `.nornir/msi-oci-routing.md`). Unlike an init-system renderer there
/// is no unit *file* — a container runtime IS the supervisor — so the whole spec
/// lands in the lifecycle **verbs as data** (the `podman run …` argv), plus, when
/// the spec carries env, a mode-0600 `--env-file` the run points at. Pure: no root,
/// no IO — the privileged driver runs the argv.
///
/// The mapping onto the container model (stated where it differs from an init unit):
/// - **Image.** A container needs an image, not a host `exec` path. `spec.image` is
///   used verbatim; when unset it falls back to `<name>:latest` — the same honest
///   placeholder the [`crate::k8s`] renderer uses (the caller should set `.image`).
/// - **Command.** `--entrypoint <exec>` sets the binary and the trailing
///   `<image> <args…>` are its CMD, so the exact neutral `exec`/`args` split projects
///   onto the container 1:1 (the image's own entrypoint is overridden — faithful to
///   the spec, documented).
/// - **Restart / boot-persistence.** `--restart=<policy>` is the container twin of an
///   init's supervision *and* its enable-at-boot: the runtime re-runs a
///   restart-policy container when it starts. [`RestartPolicy::No`]→`no`,
///   [`RestartPolicy::OnFailure`]→`on-failure`, [`RestartPolicy::Always`]→`always`.
///   `podman run` has no per-restart delay knob, so [`ServiceSpec::restart_sec`] is
///   **not** expressible here (documented, not faked — the same class of honest limit
///   as the runit/dinit/s6 backends' exit-code blindness).
/// - **Dependencies.** both `needs` and `after` become `--requires <dep>` (podman's
///   container start-ordering primitive); podman has a single ordering notion, so the
///   two fold together (deduped, order-preserving).
/// - **User.** system scope drops privileges with `--user <user>[:<group>]` (the uid
///   the process runs as *inside* the container); user scope runs rootless as the
///   caller (no `--user`).
/// - **Env.** `--env-file <path>` pointing at a mode-0600 file of literal `NAME=value`
///   lines (podman/Docker env-file format — no shell quoting/export), matching the
///   0600 env-file posture of every other backend. No env ⇒ no file and no `--env-file`.
/// - **Scope.** system: env-file at `/etc/<name>/<name>.env`, `--user` drop. user:
///   home-relative `.config/<name>/<name>.env` (the driver joins `$HOME`, mirroring the
///   systemd `--user` renderer), rootless (no `--user`).
pub struct OciRenderer;

impl OciRenderer {
    /// Restart policy → the `podman --restart` keyword.
    fn restart_flag(p: RestartPolicy) -> &'static str {
        match p {
            RestartPolicy::No => "no",
            RestartPolicy::OnFailure => "on-failure",
            RestartPolicy::Always => "always",
        }
    }
}

impl ServiceRenderer for OciRenderer {
    fn backend(&self) -> Backend {
        Backend::Oci
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        let user_scope = matches!(spec.scope, Scope::User);
        // A container needs an image; fall back to `<name>:latest` (same honest
        // placeholder as the k8s renderer) when the spec did not set one — resolved
        // through the shared [`container_image`] so OCI + Quadlet agree (L5).
        let image = container_image(spec);
        let restart = Self::restart_flag(spec.restart);
        // env-file path: system → /etc/<name>/<name>.env; user → home-relative
        // .config/<name>/<name>.env (the driver joins $HOME, like the systemd --user
        // renderer's .config/… path).
        let env_path = if user_scope {
            format!(".config/{n}/{n}.env")
        } else {
            format!("/etc/{n}/{n}.env")
        };

        // ── the `podman run` invocation, assembled as argv data ──
        let mut run: Vec<String> = vec![
            "podman".into(),
            "run".into(),
            "-d".into(),
            "--name".into(),
            n.clone(),
            format!("--restart={restart}"),
        ];
        // user: system scope drops privileges to user[:group]; user scope runs
        // rootless as the caller (no --user).
        if !user_scope {
            if let Some(u) = &spec.user {
                let val = match &spec.group {
                    Some(g) => format!("{u}:{g}"),
                    None => u.clone(),
                };
                run.push("--user".into());
                run.push(val);
            }
            // ADDITIONAL groups. Unlike the shell supervisors, an OCI runtime HAS a
            // real equivalent — `--group-add`, one flag per group — so the honest
            // thing here is to render it, not to note it as unhonoured. Kept in the
            // system-scope arm beside `--user` so every backend agrees on when a
            // privilege directive applies (a user-scope service already runs with the
            // caller's own groups).
            for g in &spec.supplementary_groups {
                run.push("--group-add".into());
                run.push(g.clone());
            }
        }
        // THE RESOURCE CEILING. An OCI runtime HAS a real route, so it is rendered,
        // not noted: `--memory` writes the container cgroup's `memory.max` — literally
        // the same file `MemoryMax=` writes — and `--memory-reservation` is the nearest
        // thing podman exposes to a soft ceiling.
        //
        // Stated honestly because it is NOT an exact twin: `--memory-reservation`
        // lands on cgroup-v2 `memory.low` (protection FROM reclaim) rather than
        // `memory.high` (throttle UNDER reclaim), and podman has no `--memory-high`.
        // The choice was "render the nearest real knob and name the difference in the
        // field's doc" or "drop the soft ceiling silently"; the second is the defect
        // this whole axis exists to stop.
        //
        // Deliberately OUTSIDE the `!user_scope` branch above: unlike `--user` and
        // `--group-add`, a memory ceiling is not a privilege directive — rootless
        // podman sets it in the user's own delegated cgroup subtree, exactly as a
        // `systemctl --user` unit does.
        if let Some(m) = &spec.memory {
            if let Some(v) = m.podman_memory() {
                run.push(format!("--memory={v}"));
            }
            if let Some(v) = m.podman_reservation() {
                run.push(format!("--memory-reservation={v}"));
            }
        }
        // WHICH container the kernel picks when the BOX is short. podman HAS the exact
        // flag — `--oom-score-adj int  Tune the host's OOM preferences (-1000 to 1000)`,
        // read off `podman run --help` on the box that generated this — so it is
        // rendered, not noted. This is the one OOM axis the OCI renderer can honour:
        // the reaction has no podman flag at all and is the argv backend's known
        // silent residue.
        if let Some(a) = spec.oom_score_adjust {
            run.push(format!("--oom-score-adj={}", a.value()));
        }
        if let Some(wd) = &spec.working_dir {
            run.push("-w".into());
            run.push(wd.clone());
        }
        if !spec.file_env().is_empty() {
            run.push("--env-file".into());
            run.push(env_path.clone());
        }
        // Dependencies: needs + after both map to --requires (podman's single
        // start-ordering notion), deduped + order-preserving.
        let mut deps: Vec<&String> = Vec::new();
        for dep in spec.needs.iter().chain(spec.after.iter()) {
            if !deps.contains(&dep) {
                deps.push(dep);
            }
        }
        for dep in deps {
            run.push("--requires".into());
            run.push(dep.clone());
        }
        // Config files: rendered on the host at their path and bind-mounted
        // read-only at the SAME path inside the container.
        for m in container_config_mounts(spec, "podman")? {
            run.push("--volume".into());
            run.push(m);
        }
        // --entrypoint <exec> sets the binary; the trailing <image> <args…> are its
        // CMD, so the neutral exec/args split projects onto the container 1:1.
        run.push("--entrypoint".into());
        run.push(spec.exec.clone());
        run.push(image);
        for a in &spec.args {
            run.push(a.clone());
        }

        // env → a mode-0600 --env-file of literal NAME=value lines (podman/Docker
        // format: no shell quoting/export), matching every backend's env-file posture.
        let mut files = Vec::new();
        if !spec.file_env().is_empty() {
            let mut env = String::new();
            for (k, v) in spec.file_env() {
                env.push_str(&literal_env_line("podman env-file", k, v)?);
            }
            files.push(RenderedFile {
                path: env_path,
                contents: env,
                mode: 0o600,
            });
        }

        Ok(RenderedService {
            backend: Backend::Oci,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            // enable-at-boot = (re)assert the container's restart policy; the runtime
            // re-runs restart-policy containers when it starts, so the policy IS the
            // boot persistence (there is no separate init-style enable).
            enable: vec![
                "podman".into(),
                "update".into(),
                format!("--restart={restart}"),
                n.clone(),
            ],
            // start now = create + run the container (also sets the restart policy).
            start: run,
            // status = the container's runtime state (Running/Exited/…).
            status: vec![
                "podman".into(),
                "container".into(),
                "inspect".into(),
                "-f".into(),
                "{{.State.Status}}".into(),
                n.clone(),
            ],
            stop: vec!["podman".into(), "stop".into(), n.clone()],
            remove: vec![
                "podman".into(),
                "rm".into(),
                "--force".into(),
                "--ignore".into(),
                n.clone(),
            ],
        })
    }
}

// ───────────────────────────── Quadlet renderer ─────────────────────────────

/// Quadlet renderer — projects the neutral [`ServiceSpec`] onto a systemd
/// **`.container` quadlet unit** (the shape `podman-systemd`, a.k.a. the `quadlet`
/// generator, consumes). Unlike the [`OciRenderer`], which hands the whole spec to
/// `podman run` argv, a quadlet is a *declarative unit file*: systemd's `quadlet`
/// generator reads `<name>.container` on `daemon-reload` and writes a real
/// `<name>.service` that `podman` supervises. So this renderer is a hybrid — it
/// REUSES the OCI image-ref handling (`container_image`) for the `[Container]`
/// body AND the systemd renderer's `[Install]`/`[Unit]` conventions for boot
/// wiring and ordering (L5) — the best of both shipped limbs, no third code path.
///
/// The mapping onto the quadlet model (stated where it differs from a raw unit):
/// - **`[Container]`.** `ContainerName=<name>`, `Image=<image>` (the shared
///   `container_image` fallback — explicit image, else `<name>:latest`),
///   `Exec=<command line>` (the container command; the neutral `exec`+`args` project
///   to it as the full command line — the image must carry the binary at that path,
///   the container-build contract, exactly like the OCI renderer's `--entrypoint`
///   exec), `WorkingDir=` when set, `User=`/`Group=` in system scope (rootless as the
///   caller in user scope, no `User=`, mirroring the OCI renderer), and inline
///   `Environment=KEY=VALUE` lines (the systemd renderer's env posture — the
///   generated `.service` inherits them; no env ⇒ no lines).
/// - **`[Service]`.** `Restart=`/`RestartSec=` via [`RestartPolicy::as_systemd`] —
///   the `podman-systemd`-generated `.service` is supervised by systemd, so systemd's
///   restart knob (NOT podman's own `--restart`) is the right one, and `restart_sec`
///   IS expressible here (unlike the raw-`podman-run` OCI backend).
/// - **`[Unit]`.** the SAME dependency conventions as the systemd renderer:
///   `network-online.target` is a system target added to `After=`/`Wants=` in system
///   scope only; `needs` → `After=`+`Wants=`, `after` → `After=`, dep names becoming
///   `<dep>.service` via `unit_name` (the one neutral→systemd dep mapping).
/// - **`[Install]`.** `WantedBy=multi-user.target` (system) / `default.target`
///   (user) — the systemd renderer's exact `[Install]` convention.
/// - **Verbs.** a quadlet is materialised into `<name>.service` by the generator on
///   `daemon-reload`, and that same reload installs the `[Install]` `WantedBy` symlink
///   — so `daemon-reload` **is** the enable-at-boot step (a quadlet has no separate
///   `systemctl enable`; documented, not faked). `start`/`status`/`stop` then target
///   the generated `<name>.service` via `systemctl [--user] …`.
/// - **Scope.** system: `/etc/containers/systemd/<name>.container`, `User=` drop,
///   `multi-user.target`, root `systemctl`. user: home-relative
///   `.config/containers/systemd/<name>.container` (the driver joins `$HOME`,
///   mirroring the systemd `--user` renderer's `.config/…` path), rootless (no
///   `User=`), `default.target`, `systemctl --user`.
pub struct QuadletRenderer;

impl ServiceRenderer for QuadletRenderer {
    fn backend(&self) -> Backend {
        Backend::Quadlet
    }

    fn render(&self, spec: &ServiceSpec) -> anyhow::Result<RenderedService> {
        spec.validate()?;
        let n = &spec.name;
        let user_scope = matches!(spec.scope, Scope::User);
        let image = container_image(spec);

        let mut s = String::new();

        // ── [Unit]: Description + ordering, reusing the systemd renderer's dep
        // conventions (network-online.target system-only; needs → After+Wants,
        // after → After; dep names → <dep>.service via unit_name) ──
        s.push_str("[Unit]\n");
        s.push_str(&format!("Description={}\n", spec.description.replace('%', "%%")));
        let mut after: Vec<String> = Vec::new();
        let mut wants: Vec<String> = Vec::new();
        if !user_scope {
            after.push("network-online.target".to_string());
            wants.push("network-online.target".to_string());
        }
        for dep in &spec.after {
            after.push(unit_name(dep));
        }
        for dep in &spec.needs {
            after.push(unit_name(dep));
            wants.push(unit_name(dep));
        }
        if !after.is_empty() {
            s.push_str(&format!("After={}\n", after.join(" ")));
        }
        if !wants.is_empty() {
            s.push_str(&format!("Wants={}\n", wants.join(" ")));
        }
        // THE RATE LIMIT — honoured, on the `[Unit]` side, through the same
        // pass-through that carries `OOMPolicy=` on the `[Service]` side. quadlet
        // copies `[Unit]` keys into the generated `.service`, and these two ARE
        // `[Unit]` keys in systemd (man systemd.unit), so this is the real directive in
        // its real section rather than a container-side approximation. podman's own
        // `--restart=on-failure:N` is a retry COUNT with no window and would have been
        // the approximation.
        if let Some(l) = spec.start_limit {
            s.push_str(&format!("StartLimitIntervalSec={}\n", l.interval_sec()));
            if l.interval_sec() != 0 {
                s.push_str(&format!("StartLimitBurst={}\n", l.burst()));
            }
        }
        s.push('\n');

        // ── [Container]: the quadlet body podman-systemd projects into the .service ──
        s.push_str("[Container]\n");
        s.push_str(&format!("ContainerName={n}\n"));
        s.push_str(&format!("Image={image}\n"));
        for m in container_config_mounts(spec, "quadlet")? {
            s.push_str(&format!("Volume={}\n", m.replace('%', "%%")));
        }
        // Exec= is the container command; the neutral exec+args project 1:1 as the
        // full command line (the image carries the binary at that path).
        // Quadlet splits Exec=/Environment= by systemd's rules (and the generated
        // unit expands `%` specifiers), so the systemd encoders are reused.
        let words: Vec<String> = std::iter::once(&spec.exec)
            .chain(&spec.args)
            .map(|w| crate::systemd::systemd_word(w))
            .collect();
        s.push_str(&format!("Exec={}\n", words.join(" ")));
        if let Some(wd) = &spec.working_dir {
            s.push_str(&format!("WorkingDir={wd}\n"));
        }
        // system scope drops privileges to the run-as user (+ group); user scope runs
        // rootless as the caller (no User=), mirroring the OCI renderer.
        if !user_scope {
            if let Some(u) = &spec.user {
                s.push_str(&format!("User={u}\n"));
                if let Some(g) = &spec.group {
                    s.push_str(&format!("Group={g}\n"));
                }
            }
            // Quadlet's own key for additional groups is `GroupAdd=` in [Container]
            // (it becomes podman's `--group-add`), NOT the systemd unit's
            // `SupplementaryGroups=` — the generated .service describes a container,
            // and the groups have to land on the container process. One key per
            // group, mirroring the OCI renderer.
            for g in &spec.supplementary_groups {
                s.push_str(&format!("GroupAdd={g}\n"));
            }
        }
        // THE RESOURCE CEILING, on the CONTAINER, not on the supervising unit. Quadlet
        // has a native `[Container]` key for the hard ceiling — `Memory=`, which the
        // generator turns into podman's `--memory` — so that is used rather than
        // `PodmanArgs=`. There is no native soft-ceiling key, so the soft half goes
        // through `PodmanArgs=--memory-reservation=`, which is the documented escape
        // hatch for a podman flag quadlet has not surfaced.
        //
        // NOT `[Service] MemoryMax=`: that is the same wrong-side-of-the-seam mistake
        // as `SupplementaryGroups=` vs `GroupAdd=` — the workload runs inside the
        // container, and a ceiling on the supervising unit is not a ceiling on it.
        //
        // Outside the `!user_scope` branch on purpose (see the OCI renderer): a
        // rootless container gets its ceiling in the caller's own delegated subtree.
        if let Some(m) = &spec.memory {
            if let Some(v) = m.podman_memory() {
                s.push_str(&format!("Memory={v}\n"));
            }
            if let Some(v) = m.podman_reservation() {
                s.push_str(&format!("PodmanArgs=--memory-reservation={v}\n"));
            }
        }
        // WHICH container the kernel picks — through `PodmanArgs=`, the same seam
        // `--memory-reservation` rides, and on the CONTAINER side deliberately. The
        // reaction went to `[Service]` because it is a decision the service manager
        // makes about a unit; this one is a property of the container's own processes,
        // so it belongs with the ceiling. Quadlet has no dedicated key for it.
        if let Some(a) = spec.oom_score_adjust {
            s.push_str(&format!("PodmanArgs=--oom-score-adj={}\n", a.value()));
        }
        // env → inline Environment= lines (the systemd renderer's posture); the
        // generated .service inherits them. No env ⇒ no lines.
        for (k, v) in &spec.env {
            s.push_str(&format!(
                "Environment={}\n",
                crate::systemd::environment_assignment(k, v)
            ));
        }
        // Secrets NEVER inline (the .container is 0644): a 0600 podman env file next to
        // it, named relative to the unit file (quadlet resolves it from there).
        if !spec.secret_env.is_empty() {
            s.push_str(&format!("EnvironmentFile={n}.env\n"));
        }
        s.push('\n');

        // ── [Service]: systemd supervises the generated unit, so Restart= is the
        // systemd knob (via as_systemd), and restart_sec IS expressible here ──
        s.push_str("[Service]\n");
        s.push_str(&format!("Restart={}\n", spec.restart.as_systemd()));
        s.push_str(&format!("RestartSec={}\n", spec.restart_sec));
        // THE REACTION — and unlike the ceiling, it belongs on THIS side of the seam.
        //
        // The memory ceiling had to go on the `[Container]` side (`Memory=` → podman's
        // `--memory`), because it is the CONTAINER's cgroup that must be bounded. The
        // OOM reaction is the opposite: `OOMPolicy=` is a decision the service manager
        // makes about a UNIT, podman has no equivalent flag at all, and quadlet passes
        // `[Service]` keys straight through to the generated `.service` — which is why
        // `Restart=`/`RestartSec=` are already rendered here rather than as
        // `--restart`. So the same pass-through carries the real systemd directive,
        // spelled exactly as the systemd renderer spells it.
        //
        // What is NOT claimed: whether a given podman puts the container's processes
        // inside the generated unit's own cgroup depends on its cgroup mode, and a kill
        // in a cgroup the unit does not own is a kill the unit's policy never sees. The
        // directive is real and correctly placed; that precondition is podman's.
        if let Some(p) = spec.oom_policy {
            s.push_str(&format!("OOMPolicy={}\n", p.as_systemd()));
        }
        s.push('\n');

        // ── [Install]: WantedBy the boot target — the systemd renderer's convention.
        s.push_str("[Install]\n");
        let wanted_by = if user_scope {
            "default.target"
        } else {
            "multi-user.target"
        };
        s.push_str(&format!("WantedBy={wanted_by}\n"));

        // File path: system → /etc/containers/systemd/<name>.container; user →
        // home-relative .config/containers/systemd/<name>.container (driver joins $HOME).
        let path = if user_scope {
            format!(".config/containers/systemd/{n}.container")
        } else {
            format!("/etc/containers/systemd/{n}.container")
        };

        // Verbs: daemon-reload generates <name>.service AND installs the [Install]
        // WantedBy symlink, so it IS the enable-at-boot step (no separate `systemctl
        // enable` for a quadlet). start/status/stop target the generated <name>.service.
        let user_flag: &[&str] = if user_scope { &["--user"] } else { &[] };
        let argv = |verbs: &[&str]| -> Vec<String> {
            let mut v: Vec<String> = vec!["systemctl".into()];
            v.extend(user_flag.iter().map(|x| x.to_string()));
            v.extend(verbs.iter().map(|x| x.to_string()));
            v
        };
        let mut files = Vec::new();
        if !spec.secret_env.is_empty() {
            let mut env = String::new();
            for (k, v) in &spec.secret_env {
                env.push_str(&literal_env_line("podman env-file", k, v)?);
            }
            let env_path = match path.rsplit_once('/') {
                Some((dir, _)) => format!("{dir}/{n}.env"),
                None => format!("{n}.env"),
            };
            files.push(RenderedFile {
                path: env_path,
                contents: env,
                mode: 0o600,
            });
        }
        files.insert(
            0,
            RenderedFile {
                path,
                contents: s,
                mode: 0o644,
            },
        );
        let unit = format!("{n}.service");
        Ok(RenderedService {
            backend: Backend::Quadlet,
            files: files.into_iter().chain(config_rendered_files(spec)).collect(),
            enable: argv(&["daemon-reload"]),
            start: argv(&["start", &unit]),
            status: argv(&["is-active", &unit]),
            stop: argv(&["stop", &unit]),
            // A generated unit cannot be disabled: once the `.container` file is
            // gone, a daemon-reload re-runs the generator without it.
            remove: argv(&["daemon-reload"]),
        })
    }
}

/// **The ONE `NOT HONOURED` note for a declared [`ServiceSpec::memory`] a backend
/// cannot render.**
///
/// One function so the five backends without a cgroup route say the same thing in
/// the same words, and so "what an unhonoured ceiling looks like in the output"
/// cannot drift into five slightly different sentences. Same discipline as the
/// single OpenRC supplementary-groups note: a gap NAMED beats a gap HIDDEN, and the
/// note has to carry the NUMBERS or an operator cannot act on it.
///
/// `prefix` is the target's comment lead-in (`"# "` for a shell script, `"    "`
/// inside an XML comment); `backend` names the init system; `nearest` is the
/// backend-specific paragraph about what it *does* have and why that is not this.
#[doc(hidden)]
pub fn memory_not_honoured_note(
    prefix: &str,
    backend: &str,
    nearest: &[&str],
    m: &MemoryBound,
) -> String {
    let mut lines: Vec<String> = vec![
        "skidbladnir: NOT HONOURED — this service DECLARED a memory ceiling and".to_string(),
        format!("{backend} has no cgroup memory ceiling to render it onto."),
        format!("  DECLARED: {}", m.describe()),
    ];
    lines.extend(nearest.iter().map(|l| (*l).to_string()));
    lines
        .push("Bound this service on the host instead (a systemd slice, a direct cgroup-v2".into());
    lines.push("ceiling, or a container runtime). Until then it is UNBOUNDED: the".into());
    lines.push("failure mode is not \"the service dies\", it is \"the box dies\" — dwarf,".into());
    lines.push("2026-08-20, 33 GB RSS, two services OOM-killed and the machine down.".into());

    let mut out = String::new();
    for l in lines {
        out.push_str(prefix);
        out.push_str(&l);
        out.push('\n');
    }
    out
}

/// **The ONE `NOT HONOURED` note for a declared [`ServiceSpec::oom_score_adjust`] a
/// backend cannot render.**
///
/// Fourth and last of its kind. Unlike the other three it hands over a COMMAND rather
/// than a description, because for once the by-hand route is short, portable and
/// VERIFIED: `/proc/self/oom_score_adj` is a plain writable file, measured on the box
/// that generated this note.
///
/// It also carries the privilege rule, measured with `CapEff: 0000000000000000`:
/// raising the value succeeded, a negative value FAILED. An operator handed only the
/// number would write the protective direction into a script that has already dropped
/// privileges and get silence.
#[doc(hidden)]
pub fn oom_score_not_honoured_note(prefix: &str, backend: &str, a: OomScoreAdjust) -> String {
    let mut lines: Vec<String> = vec![
        "skidbladnir: NOT HONOURED — this service DECLARED how likely the kernel should"
            .to_string(),
        format!("be to pick IT when the whole box runs out of memory, and {backend}"),
        "has no directive to render that onto.".to_string(),
        format!(
            "  DECLARED: OOMScoreAdjust={a} ({})",
            if a.value() < 0 {
                "protect this service, reap the neighbours first"
            } else if a.value() > 0 {
                "sacrifice this service before the neighbours"
            } else {
                "rank with everything else"
            }
        ),
        "BY HAND, and this route is VERIFIED rather than guessed at — it is a plain".to_string(),
        "writable file, not a supervisor feature, so it works under any init:".to_string(),
        format!("      echo {a} > /proc/self/oom_score_adj"),
        "in the run script BEFORE the exec, and before any privilege drop. The value".to_string(),
        "survives execve and setuid, so writing it early is what makes it stick.".to_string(),
    ];
    if a.needs_privilege() {
        lines.push(
            "  CAUTION: this is the PROTECTIVE direction and it needs CAP_SYS_RESOURCE.".into(),
        );
        lines.push("  MEASURED with no capabilities: raising 200->500 succeeded, lowering".into());
        lines.push("  500->100 succeeded, -500 FAILED. Write it while still root.".into());
    }
    let mut out = String::new();
    for l in lines {
        out.push_str(prefix);
        out.push_str(&l);
        out.push('\n');
    }
    out
}

/// **The ONE `NOT HONOURED` note for a declared [`ServiceSpec::start_limit`] a backend
/// cannot render.**
///
/// Third of its kind, deliberately identical in shape to
/// [`memory_not_honoured_note`] and [`oom_not_honoured_note`].
///
/// It carries something the other two do not: the DERIVED OUTCOME. A rate limit is
/// two numbers whose meaning is a third thing — whether the service eventually parks
/// or retries forever — and an operator granting it by hand on another init system
/// needs the outcome, not the arithmetic, or they will reproduce the numbers and get
/// the opposite behaviour.
///
/// Every `nearest` paragraph passed to this function says **UNVERIFIED** where it
/// names a candidate knob, and that is not hedging. None of these init systems is
/// installed on the box this was written on, so the names could not be checked
/// against a live host or a man page — and a misspelled variable in an OpenRC conf or
/// a dinit service file is SILENTLY IGNORED, which would leave a declared limit
/// looking honoured while doing nothing. A named note is discoverable; a silently
/// dead directive is not. Same ruling as the OpenRC OOM partial.
#[doc(hidden)]
pub fn start_limit_not_honoured_note(
    prefix: &str,
    backend: &str,
    nearest: &[&str],
    l: StartLimit,
    restart_sec: u32,
) -> String {
    let mut lines: Vec<String> = vec![
        "skidbladnir: NOT HONOURED — this service DECLARED how many times it may be".to_string(),
        format!("restarted before giving up, and {backend}"),
        "has no start rate limiter to render that onto.".to_string(),
    ];
    if l.interval_sec() == 0 {
        lines.push("  DECLARED: no limit at all (StartLimitIntervalSec=0) — this service".into());
        lines.push("  is meant to RETRY FOREVER rather than ever park in a failed state.".into());
    } else {
        lines.push(format!(
            "  DECLARED: StartLimitIntervalSec={} StartLimitBurst={} beside RestartSec={}",
            l.interval_sec(),
            l.burst(),
            restart_sec
        ));
        lines.push(format!(
            "  MEANING:  give up after {} attempts ({}s apart, so attempt {} falls at {}s,",
            l.burst(),
            restart_sec,
            l.burst() + 1,
            u64::from(restart_sec) * u64::from(l.burst())
        ));
        lines.push(format!(
            "            inside the {}s window) and STAY DOWN in a failed state.",
            l.interval_sec()
        ));
    }
    lines.extend(nearest.iter().map(|l| (*l).to_string()));
    lines.push("Ungranted, this service retries forever and a permanently broken one is".into());
    lines.push("indistinguishable from a healthy one to anything that is not watching.".into());

    let mut out = String::new();
    for l in lines {
        out.push_str(prefix);
        out.push_str(&l);
        out.push('\n');
    }
    out
}

/// **The ONE `NOT HONOURED` note for a declared [`ServiceSpec::oom_policy`] a backend
/// cannot render.**
///
/// The twin of [`memory_not_honoured_note`], deliberately built the same way and for
/// the same reason: the backends that cannot react to an OOM kill must say so in the
/// SAME words, in their own output, carrying the DECLARED reaction — an operator who
/// cannot read what was asked for cannot grant it by hand.
///
/// It says something narrower than the memory note, and the difference matters: an
/// unhonoured ceiling leaves the box exposed, while an unhonoured reaction leaves the
/// service's behaviour on the day it IS killed decided by the supervisor's own
/// reflexes rather than by the declaration.
#[doc(hidden)]
pub fn oom_not_honoured_note(prefix: &str, backend: &str, nearest: &[&str], p: OomPolicy) -> String {
    let mut lines: Vec<String> = vec![
        "skidbladnir: NOT HONOURED — this service DECLARED what should happen to it".to_string(),
        format!("when the OOM killer reaps one of its processes, and {backend}"),
        "has no cgroup OOM integration to render that onto.".to_string(),
        format!("  DECLARED: OOMPolicy={p} ({})", oom_policy_meaning(p)),
    ];
    lines.extend(nearest.iter().map(|l| (*l).to_string()));
    lines.push("Until it is granted on the host, what happens after a kill is whatever".into());
    lines.push("this supervisor does by reflex — which for a service that spawns".into());
    lines.push("children, or one that IS its memory, is the opposite of the two.".into());

    let mut out = String::new();
    for l in lines {
        out.push_str(prefix);
        out.push_str(&l);
        out.push('\n');
    }
    out
}

/// The one-clause human gloss of each reaction, so a `NOT HONOURED` note says what
/// the operator has to reproduce and not merely which keyword was typed.
fn oom_policy_meaning(p: OomPolicy) -> &'static str {
    match p {
        OomPolicy::Continue => "log the kill, the service KEEPS RUNNING",
        OomPolicy::Stop => "stop the whole service cleanly, then let the restart policy apply",
        OomPolicy::Kill => "reap every remaining process of the service too",
    }
}

/// The `nearest` paragraph shared by runit / s6-rc / dinit / SysV: none of them has a
/// cgroup OOM notion at all. Their rlimit knobs make an ALLOCATION FAIL inside the
/// program rather than have the kernel reap a cgroup, so there is no kill event for a
/// policy to react to — the same reason they cannot honour the ceiling.
const NO_CGROUP_OOM_INTEGRATION: &[&str] = &[
    "There is no near-miss to render here: its rlimit knob makes malloc fail INSIDE",
    "the process, which is not a kernel OOM kill and produces no event a supervisor",
    "could react to. Reproduce it on the host — a systemd slice, a direct cgroup-v2",
    "memory.oom.group, or a container runtime that owns the service's cgroup.",
];

/// The `nearest` paragraph shared by runit / s6-rc / dinit / SysV: they all expose a
/// POSIX **rlimit**, which looks like the answer and is a different mechanism.
const RLIMIT_IS_NOT_A_CGROUP_CEILING: &[&str] = &[
    "The rlimit knob it does have is NOT the same mechanism: an rlimit caps one",
    "process's ADDRESS SPACE, is accounted per-process rather than per-service,",
    "never reclaims, has no soft ceiling at all, and when it bites the allocation",
    "fails INSIDE the program (malloc returns NULL) instead of the cgroup being",
    "killed cleanly and restarted. Rendering it here would claim a bound that is not",
    "one and leave the box exactly as exposed.",
];

/// A dep *service name* → the systemd *unit name* (append `.service` unless it
/// already names a unit type, e.g. a `.target` or an explicit `.service`).
fn unit_name(dep: &str) -> String {
    if dep.contains('.') {
        dep.to_string()
    } else {
        format!("{dep}.service")
    }
}

// ─────────────────────────────── Lifecycle ──────────────────────────────────

/// The SINGLE service-lifecycle expert — the ship. A consumer (holger, nornir,
/// dwarfs) declares a [`ServiceSpec`] and hands it to a `Lifecycle`; the
/// `Lifecycle` runs install / enable / start / stop / reload / status / health /
/// uninstall by delegating to the systemd limb ([`crate::systemd`]) — the pure
/// [`crate::systemd::systemd_unit_scoped`] renderer for the unit text and the
/// pure-Rust D-Bus [`crate::systemd::Systemd`] controller (`org.freedesktop.systemd1`,
/// no `systemctl` subprocess) for the verbs. No consumer hand-rolls systemd.
///
/// The scope (system vs `--user`) is taken from [`ServiceSpec::scope`], so the
/// SAME spec drives a root system service or a per-user dev service. `health()`
/// is the one method that does not touch D-Bus — it actively probes the
/// [`ServiceSpec::health`] endpoint (TCP connect / HTTP GET) and therefore works
/// on any platform.
#[derive(Debug, Clone)]
pub struct Lifecycle {
    spec: ServiceSpec,
}

impl Lifecycle {
    /// Wrap a [`ServiceSpec`]. The spec's [`ServiceSpec::scope`] selects the
    /// systemd manager (system bus vs the per-user session bus).
    pub fn new(spec: ServiceSpec) -> Self {
        Lifecycle { spec }
    }

    /// The spec this lifecycle drives.
    pub fn spec(&self) -> &ServiceSpec {
        &self.spec
    }

    /// The systemd unit name (`<name>.service`).
    pub fn unit(&self) -> String {
        format!("{}.service", self.spec.name)
    }

    /// The systemd `.service` unit text this lifecycle installs (pure — no root,
    /// no bus). Delegates to the ONE renderer via [`ServiceSpec::render_systemd_unit`].
    pub fn unit_text(&self) -> String {
        self.spec.render_systemd_unit()
    }

    /// The systemd-specific spec the privileged limb consumes.
    ///
    /// PUBLIC since the 2026-07-22 incident: a consumer must be able to assert, in
    /// its own test suite and without root, exactly which directory its install will
    /// create and re-own — that is
    /// [`SystemdServiceSpec::service_home`](crate::systemd::SystemdServiceSpec::service_home),
    /// and it is NOT the rendered `WorkingDirectory=`.
    pub fn systemd_spec(&self) -> crate::systemd::SystemdServiceSpec {
        to_systemd_spec(&self.spec)
    }

    /// INSTALL — write the unit for the spec's scope, `daemon-reload`, enable at
    /// boot, and start it now (the full first-time bring-up). Delegates to
    /// [`crate::systemd::install_systemd_scoped`]. Root for system scope.
    #[cfg(unix)]
    pub fn install(&self) -> anyhow::Result<()> {
        crate::systemd::install_systemd_scoped(&self.systemd_spec(), self.spec.scope)
    }

    /// ENABLE start-at-boot (no start now). D-Bus `EnableUnitFiles`.
    #[cfg(unix)]
    pub fn enable(&self) -> anyhow::Result<()> {
        crate::systemd::Systemd::connect_scoped(self.spec.scope)?.enable(&self.unit())
    }

    /// START the unit now. D-Bus `StartUnit`.
    #[cfg(unix)]
    pub fn start(&self) -> anyhow::Result<()> {
        crate::systemd::Systemd::connect_scoped(self.spec.scope)?.start(&self.unit())
    }

    /// STOP the unit now. D-Bus `StopUnit`.
    ///
    /// ⚠ `Ok(())` here means **the manager accepted the request**, not that the
    /// service stopped cleanly. Prefer [`stop_reported`](Lifecycle::stop_reported) for
    /// any verb whose exit code an operator or a script will read: on oden 2026-08-27
    /// a `--stop` built on this method printed `stopped (korp-server.service)` and
    /// exited 0 while the main process dumped core on `SIGSEGV` and the unit settled
    /// `failed`.
    #[cfg(unix)]
    pub fn stop(&self) -> anyhow::Result<()> {
        crate::systemd::Systemd::connect_scoped(self.spec.scope)?.stop(&self.unit())
    }

    /// **STOP and report what happened to the PROCESS** — the verb a lifecycle CLI
    /// should call. Delegates to
    /// [`Systemd::stop_reported`](crate::systemd::Systemd::stop_reported), which
    /// snapshots the unit's verdict, stops it, waits for it to actually settle, and
    /// reads `Result`/`ExecMainCode`/`ExecMainStatus` back off the bus.
    ///
    /// The returned [`StopReport`](crate::systemd::StopReport) carries systemd's own
    /// verdict; `is_clean()` is false over a core dump, a timeout or a non-zero exit,
    /// and `summary()` never prints the bare word "stopped" over one. The default
    /// budget is the same 10 s a stop timeout gets elsewhere in the ship — long enough
    /// for a service that drains, short enough that a verb still returns.
    #[cfg(unix)]
    pub fn stop_reported(&self) -> anyhow::Result<crate::systemd::StopReport> {
        self.stop_reported_within(std::time::Duration::from_secs(10))
    }

    /// [`stop_reported`](Lifecycle::stop_reported) with an explicit settle budget.
    #[cfg(unix)]
    pub fn stop_reported_within(
        &self,
        budget: std::time::Duration,
    ) -> anyhow::Result<crate::systemd::StopReport> {
        crate::systemd::Systemd::connect_scoped(self.spec.scope)?
            .stop_reported(&self.unit(), budget)
    }

    /// RELOAD the manager's unit files (`daemon-reload`) — call after the unit
    /// text changes. D-Bus `Reload`.
    #[cfg(unix)]
    pub fn reload(&self) -> anyhow::Result<()> {
        crate::systemd::Systemd::connect_scoped(self.spec.scope)?.reload()
    }

    /// STATUS — is the unit currently `active`? D-Bus `ActiveState`. An
    /// unknown/inactive/failed unit is `false`, not an error.
    #[cfg(unix)]
    pub fn status(&self) -> anyhow::Result<bool> {
        crate::systemd::status_systemd_scoped(&self.systemd_spec(), self.spec.scope)
    }

    /// **UNINSTALL — kills the DATA, keeps the KEYS. Unconditionally.**
    ///
    /// > *"`uninstall` WILL ALWAYS AND MUST ALWAYS KILL DATA BUT KEEP ssh keys!"*
    ///
    /// Stop + disable the unit, remove its unit file, reload — **and then erase the
    /// service's state**, sparing its `.ssh` deploy key and any stored `token`
    /// ([`crate::uninstall::IDENTITY_NAMES`]). No flag gates the data erase; there is
    /// no `--purge`. This is THE UNINSTALL LAW and it lives in the ship so nornir,
    /// holger, dwarves and korp inherit ONE behaviour instead of inventing a fifth.
    ///
    /// ⚠ **BEHAVIOUR CHANGE (skidbladnir 0.1.13).** This verb previously left the data
    /// in place. It now erases it. A consumer that called `uninstall()` to remove only
    /// the unit must stop doing so.
    ///
    /// What it erases is the ONE declared [`ServiceSpec::data_root`] — the same
    /// absolute directory whoever owns the process, never derived from `$HOME` and
    /// never branched on [`Scope`]. A spec that declares no data root erases nothing
    /// and SAYS so. Any [`ServiceSpec::legacy_data_roots`] are swept too, by the same
    /// law, and named in the report.
    ///
    /// Returns an honest [`crate::uninstall::UninstallReport`]: what was erased, that
    /// the key was KEPT, and — when nothing happened —
    /// [`UninstallReport::is_noop`](crate::uninstall::UninstallReport::is_noop) plus a
    /// note saying why. Root for system scope.
    #[cfg(unix)]
    pub fn uninstall(&self) -> anyhow::Result<crate::uninstall::UninstallReport> {
        self.uninstall_with(crate::uninstall::UninstallOptions::new())
    }

    /// [`Lifecycle::uninstall`] with the explicit opt-ins.
    ///
    /// ⚠ [`UninstallOptions::wipe_identity`](crate::uninstall::UninstallOptions::wipe_identity)
    /// **destroys the ssh deploy key that is registered on the git remotes.** After it,
    /// a reinstall cannot clone anything until a human generates a new key and
    /// registers it on **every** private repo. It is impossible to trigger as a side
    /// effect — the plain [`Lifecycle::uninstall`] always passes the law's default —
    /// and
    /// [`UninstallOptions::remove_account`](crate::uninstall::UninstallOptions::remove_account)
    /// without it is REFUSED outright (holger's uninstall removed the home and then
    /// the account, destroying the key twice over).
    #[cfg(unix)]
    pub fn uninstall_with(
        &self,
        opts: crate::uninstall::UninstallOptions,
    ) -> anyhow::Result<crate::uninstall::UninstallReport> {
        crate::systemd::remove_systemd_scoped_with(&self.systemd_spec(), self.spec.scope, opts)
    }

    /// The erase PLAN this lifecycle's `uninstall` would execute — resolved, but
    /// nothing deleted.
    ///
    /// PUBLIC for the same reason [`Lifecycle::systemd_spec`] is: a consumer must be
    /// able to assert, in its own test suite and without root, exactly which
    /// directories its uninstall will erase — and specifically that the answer does
    /// not move when `$HOME` or the owning account moves.
    pub fn state_plan(&self) -> crate::uninstall::StatePlan {
        let sys = self.systemd_spec();
        crate::uninstall::plan_state_erase(&sys.state_spec(), std::path::Path::new("/etc/passwd"))
    }

    /// HEALTH — actively probe the service's declared [`HealthProbe`]. Unlike the
    /// other verbs this touches NO D-Bus (it opens a socket), so it works on any
    /// platform and verifies the process is really answering, not merely `active`.
    ///
    /// - [`HealthProbe::None`] ⇒ no probe declared; on unix it falls back to
    ///   [`Lifecycle::status`] (is the unit active), elsewhere `true`.
    /// - [`HealthProbe::Tcp`] ⇒ a TCP connect to `127.0.0.1:<port>` succeeds.
    /// - [`HealthProbe::TcpAt`] ⇒ a TCP connect to the **bound** `<host>:<port>`
    ///   succeeds (a wildcard host — `0.0.0.0`/`::` — probes loopback). This is
    ///   the arm an off-loopback service (tailscale/LAN address) needs: the
    ///   loopback-only [`HealthProbe::Tcp`] would call it dead while it serves.
    /// - [`HealthProbe::Http`] ⇒ an HTTP `GET <path>` on the service's first
    ///   listening [`Socket`] returns a `2xx` status.
    pub fn health(&self) -> anyhow::Result<bool> {
        match &self.spec.health {
            HealthProbe::None => {
                #[cfg(unix)]
                {
                    self.status()
                }
                #[cfg(not(unix))]
                {
                    Ok(true)
                }
            }
            HealthProbe::Tcp(port) => Ok(tcp_probe(*port)),
            HealthProbe::TcpAt { host, port } => Ok(tcp_probe_at(host, *port)),
            HealthProbe::Http(path) => {
                let port = self.spec.sockets.first().map(|s| s.port).ok_or_else(|| {
                    anyhow::anyhow!(
                        "HTTP health probe for `{}` needs a listening socket (none declared)",
                        self.spec.name
                    )
                })?;
                Ok(http_probe(port, path))
            }
            HealthProbe::HttpAt { host, path } => {
                // The same socket requirement as `Http`, and the same error — the host says
                // WHERE, the declared socket still says which port.
                let port = self.spec.sockets.first().map(|s| s.port).ok_or_else(|| {
                    anyhow::anyhow!(
                        "HTTP health probe for `{}` needs a listening socket (none declared)",
                        self.spec.name
                    )
                })?;
                Ok(http_probe_at(host, port, path))
            }
            HealthProbe::Banner { host, port, expect } => {
                Ok(banner_probe_at(host, *port, expect).is_some())
            }
        }
    }

    /// **The greeting a [`HealthProbe::Banner`] service actually sent** — the applied
    /// output behind the boolean [`health`](Lifecycle::health) returns, so a report
    /// can quote what answered instead of asserting that something did.
    ///
    /// `None` when nothing connected, nothing was said, or the spec declares a
    /// different probe. It does NOT check `expect` — that is
    /// [`health`](Lifecycle::health)'s judgement; this hands over the evidence.
    pub fn banner(&self) -> Option<String> {
        match &self.spec.health {
            HealthProbe::Banner { host, port, .. } => banner_probe_at(host, *port, ""),
            _ => None,
        }
    }

    // `refresh` and `watch_refresh` live with their engines in `skidbladnir`
    // (`service_backend::ServiceRefresh` and `service::LifecycleWatch`): the
    // install, migrate and git-watch machinery is not a systemd host's concern.
}

/// TCP health: can we open `127.0.0.1:<port>` within 2s? Pure std, no deps.
///
/// The loopback special case of [`tcp_probe_at`], kept as its own name because
/// [`HealthProbe::Tcp`] means loopback BY DEFINITION (every existing caller declares
/// a loopback-bound service). A service bound elsewhere declares
/// [`HealthProbe::TcpAt`] and lands in [`tcp_probe_at`] instead.
fn tcp_probe(port: u16) -> bool {
    tcp_probe_at("127.0.0.1", port)
}

/// TCP health against the address the service ACTUALLY binds: can we open
/// `<host>:<port>` within 2s? Pure std, no deps.
///
/// - A **wildcard** host — `0.0.0.0`, `::`, `[::]`, `*`, or empty — is not a peer you
///   can dial; it means "every interface", and such a service is reachable on
///   loopback. So the probe tries `127.0.0.1` and then `::1` (the second covers a
///   v6-only-socket bind), and is healthy if EITHER connects.
/// - Any other host is dialled verbatim. A bare IPv6 literal is bracketed for the
///   `host:port` parse, and `[…]` brackets already present are accepted.
/// - `to_socket_addrs` may resolve a name to several addresses; as before, ANY one
///   of them connecting is healthy.
fn tcp_probe_at(host: &str, port: u16) -> bool {
    if probe_host_is_wildcard(host) {
        return connect_once("127.0.0.1", port) || connect_once("::1", port);
    }
    connect_once(&resolve_probe_host(host), port)
}

/// **Is this a wildcard bind?** — `0.0.0.0`, `[::]`, `*`, or empty.
///
/// Such a service is reachable on loopback and connecting to `0.0.0.0` means nothing, so
/// every probe folds these to `127.0.0.1`. ONE writer, because the TCP and HTTP probes
/// answering this differently is precisely the class of drift this module keeps finding.
fn probe_host_is_wildcard(host: &str) -> bool {
    let bare = strip_brackets(host.trim());
    bare.is_empty()
        || bare == "*"
        || bare
            .parse::<std::net::IpAddr>()
            .is_ok_and(|ip| ip.is_unspecified())
}

/// The BARE host a probe should dial: brackets stripped, wildcards folded to loopback.
fn resolve_probe_host(host: &str) -> String {
    if probe_host_is_wildcard(host) {
        return "127.0.0.1".to_string();
    }
    strip_brackets(host.trim()).to_string()
}

/// `[::1]` → `::1`; anything else unchanged. Shared so the bracket rule has one spelling.
fn strip_brackets(host: &str) -> &str {
    host.strip_prefix('[')
        .and_then(|h| h.strip_suffix(']'))
        .unwrap_or(host)
}

/// One `host:port` connect attempt with the 2s probe timeout — resolves through
/// `to_socket_addrs` (a name can yield several addresses; ANY connecting is healthy).
/// `host` is a BARE host (no brackets); an IPv6 literal is bracketed here.
fn connect_once(host: &str, port: u16) -> bool {
    use std::net::{TcpStream, ToSocketAddrs};
    use std::time::Duration;
    let hostport = if host.contains(':') {
        format!("[{host}]:{port}") // IPv6 literal
    } else {
        format!("{host}:{port}")
    };
    let Ok(mut addrs) = hostport.to_socket_addrs() else {
        return false;
    };
    addrs.any(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok())
}

/// **Connect and read the server's unprompted greeting.** Returns the bytes it said
/// (lossy UTF-8, trimmed) when they contain `expect`, `None` otherwise — so a caller
/// gets the APPLIED OUTPUT on success rather than a bare `true`.
///
/// Host handling is the shared [`resolve_probe_host`] rule (brackets stripped,
/// wildcard binds folded to loopback), identical to the TCP and HTTP probes — three
/// probes disagreeing about what `0.0.0.0` means is the drift this module keeps
/// finding. The 2 s read timeout is the same budget the other probes use; a protocol
/// that says nothing first (HTTP, gRPC) simply times out and reports `None`, which is
/// the honest answer for "this is not a greeting protocol".
///
/// `expect: ""` matches any non-empty greeting — that is how
/// [`Lifecycle::banner`] fetches the evidence without also judging it.
fn banner_probe_at(host: &str, port: u16, expect: &str) -> Option<String> {
    use std::io::Read;
    use std::net::TcpStream;
    use std::time::Duration;
    let host = resolve_probe_host(host);
    let hostport = if host.contains(':') {
        format!("[{host}]:{port}") // IPv6 literal
    } else {
        format!("{host}:{port}")
    };
    let mut stream = TcpStream::connect(&hostport).ok()?;
    let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
    // One read is enough: a protocol greeting is written in a single small burst, and
    // reading to EOF would wait for the server to hang up — which a healthy server
    // never does.
    let mut buf = [0u8; 256];
    let n = stream.read(&mut buf).ok()?;
    if n == 0 {
        return None; // connected, then the peer closed without a word
    }
    let said = String::from_utf8_lossy(&buf[..n]).trim().to_string();
    said.contains(expect).then_some(said)
}

/// HTTP health: raw `GET <path> HTTP/1.0` to `127.0.0.1:<port>`, read the status
/// line, and accept a `2xx`. Hand-rolled over `std::net` so the lean core pulls no
/// HTTP client (mirrors the crate's `mockhttp` posture). Any connect/IO error, or
/// a non-2xx status, is unhealthy.
fn http_probe(port: u16, path: &str) -> bool {
    http_probe_at("127.0.0.1", port, path)
}

/// **HTTP health against a DECLARED host** — the twin of [`tcp_probe_at`], and the fix for
/// the same hardcode.
///
/// [`http_probe`] read `TcpStream::connect((Ipv4Addr::LOCALHOST, port))`, so a service bound
/// to a tailscale or LAN address was probed at an address it never bound and reported
/// unhealthy while serving perfectly. `HealthProbe::Http` still means loopback — it is now
/// this function with `"127.0.0.1"`, so that path is byte-for-byte what it always was.
///
/// Host handling is shared with the TCP probe ([`resolve_probe_host`]): brackets stripped,
/// wildcard binds folded to loopback, everything else dialled verbatim. The `Host:` header
/// carries the SAME host that was dialled — a probe that connects to one address and then
/// claims another in the header is exactly how a vhost-routed service answers 404 to its own
/// health check.
fn http_probe_at(host: &str, port: u16, path: &str) -> bool {
    use std::io::{Read, Write};
    use std::net::TcpStream;
    use std::time::Duration;
    let path = if path.starts_with('/') {
        path.to_string()
    } else {
        format!("/{path}")
    };
    let host = resolve_probe_host(host);
    // An IPv6 literal must be bracketed in a `Host:` header and in `to_socket_addrs`.
    let hostport = if host.contains(':') {
        format!("[{host}]:{port}")
    } else {
        format!("{host}:{port}")
    };
    let Ok(mut stream) = TcpStream::connect(&hostport) else {
        return false;
    };
    let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
    let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
    // `Host:` is the address we actually dialled, not a hardcoded `127.0.0.1` — see the
    // note on this function about vhost-routed services 404-ing their own health check.
    let req = format!("GET {path} HTTP/1.0\r\nHost: {hostport}\r\nConnection: close\r\n\r\n");
    if stream.write_all(req.as_bytes()).is_err() {
        return false;
    }
    let mut buf = [0u8; 128];
    let Ok(n) = stream.read(&mut buf) else {
        return false;
    };
    // Status line: `HTTP/1.x <code> <reason>` — accept 2xx.
    let head = String::from_utf8_lossy(&buf[..n]);
    let mut parts = head.split_whitespace();
    let _proto = parts.next();
    matches!(parts.next(), Some(code) if code.starts_with('2'))
}

#[cfg(test)]
mod tests {
    use super::*;


    #[test]
    fn config_file_paths_must_be_absolute_normal_and_unique() {
        let base = || ServiceSpec::new("bifrost", "B", "/b");
        for bad in ["etc/m.yml", "/etc/../m.yml", "/etc//m.yml", "/etc/m.yml/", "/etc/\nm"] {
            let e = base().config_file(bad, "x").validate().unwrap_err();
            assert!(e.to_string().contains("config file"), "{bad:?}: {e}");
        }
        let twice = base().config_file("/etc/m.yml", "a").config_file("/etc/m.yml", "b");
        assert!(twice.validate().unwrap_err().to_string().contains("twice"));
        let mut setuid = base().config_file("/etc/m.yml", "a");
        setuid.config_files[0].mode = 0o4755;
        assert!(setuid.validate().is_err());
        assert!(base().config_file("/etc/m.yml", "a").validate().is_ok());
    }

    /// **★ `--env KEY=VALUE`: every refusal, and both spellings.**
    ///
    /// This parser is the sanctioned replacement for a hand-written
    /// `<unit>.service.d/*.conf`, so its failure mode matters more than its happy path:
    /// a malformed pair silently DROPPED installs a unit missing exactly the variable
    /// the operator ran the command for, and prints success. Each refusal below is a
    /// separate red — an implementation that `filter_map`s bad pairs away passes the
    /// first two assertions and fails the last three.
    #[test]
    fn env_args_refuses_a_malformed_pair_instead_of_dropping_it() {
        let ok = env_args_from_argv(&[
            "--install",
            "--env",
            "KORP_SERVER_ADDR=0.0.0.0:50051",
            "--system",
            "--env=KORP_MAP_REPACK=sweden",
            "--env",
            "EMPTY=",
        ])
        .expect("three well-formed pairs");
        assert_eq!(

The file goes on; the console shows all of it.