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

mikronova/modgunn

src/service.rs

//! **Service seam** — modgunn's world-delta gatekeeper declared as a
//! [`skidbladnir::service::ServiceSpec`] driven through the ship's
//! [`skidbladnir::service::Lifecycle`], instead of a hand-rolled systemd unit. This
//! is modgunn's slice of the constellation-wide COMMON FRAMEWORK lifecycle: it
//! mirrors korp's `korp_server_spec`, nornir's `nornir_server_spec`, and holger's
//! server spec — modgunn declares *what* the service is; skidbladnir renders *how* it
//! runs (the SAME systemd/OpenRC renderer + the zbus D-Bus lifecycle, no `systemctl`
//! shell — the zero-shell charter) and probes its health. Every service runs as its
//! OWN dedicated system user from this SAME shared code (`nornir`→`nornir`,
//! `holger`→`holger`, `modgunn`→`modgunn`) via
//! [`skidbladnir::systemd::ensure_system_user`] (L5).
//!
//! **Additive + declared.** modgunn is today a CLI gate (Engine A) + a heavy
//! Engine-B world-delta engine; it has no long-running `serve` loop yet. This module
//! wires the SHARED lifecycle (the "if it has none, wire the shared one" branch): the
//! spec is the DECLARED contract a systemd unit would run for the future world-delta
//! *watcher* daemon (`ExecStart = modgunn world-delta watch`). Rendering the unit,
//! the dedicated `modgunn` user, the TCP health probe, and the fat/thin posture are
//! all live today (see the tests); standing up the watcher daemon itself is deferred
//! (logged in `workspace_nordisk/modgunn_agents_problems.md`).

use skidbladnir::service::{AgentMode, HealthProbe, ServiceSpec, Socket};
use skidbladnir::systemd::{RestartPolicy, Scope};

/// modgunn world-delta watcher's default listen address (the CDC/health socket a fat
/// Engine-B site would answer on).
pub const DEFAULT_ADDR: &str = "127.0.0.1:50071";

/// The dedicated system user modgunn's service runs as — its OWN user from the SAME
/// shared lifecycle code (L5), never shared with nornir/holger. Provisioned at
/// install time via [`skidbladnir::systemd::ensure_system_user`].
pub const SERVICE_USER: &str = "modgunn";

/// Parse the TCP port out of a `host:port` listen address (falls back to modgunn's
/// default world-delta port when the string has no parseable port).
pub fn addr_port(addr: &str) -> u16 {
    addr.rsplit(':')
        .next()
        .and_then(|p| p.parse().ok())
        .unwrap_or(50071)
}

/// Declare modgunn's world-delta gatekeeper service as a backend-neutral
/// [`ServiceSpec`].
///
/// - `exec` — the installed `modgunn` binary path (the unit's `ExecStart`; args
///   `world-delta watch`).
/// - `addr` — the listen address; its port becomes the declared [`Socket`] and the
///   [`HealthProbe::Tcp`] the ship uses to verify the service is really up.
/// - `run_as` — the system user (`None` ⇒ the caller, the only valid choice for
///   [`Scope::User`]); a system unit runs as the dedicated [`SERVICE_USER`].
/// - `user_scope` — `true` ⇒ a `systemctl --user` dev service (runs as YOU); `false`
///   ⇒ a system service under `run_as`.
///
/// The world-delta site is the **fat** node (holds the world-snapshot state);
/// verdict-gate consumers are thin. That posture is metadata only ([`AgentMode::Fat`])
/// — it does not change the rendered unit.
pub fn modgunn_service_spec(
    exec: &str,
    addr: &str,
    run_as: Option<&str>,
    user_scope: bool,
) -> ServiceSpec {
    let scope = if user_scope {
        Scope::User
    } else {
        Scope::System
    };
    let port = addr_port(addr);
    let mut spec = ServiceSpec::new(
        "modgunn",
        "Móðguðr — the nordisk security + SCIP world-delta gatekeeper",
        exec,
    )
    .scope(scope)
    .env("MODGUNN_WORLDDELTA_ADDR", addr)
    .restart(RestartPolicy::OnFailure)
    .socket(Socket::tcp(port))
    .health(HealthProbe::Tcp(port))
    .agent(AgentMode::Fat);
    // The declared ExecStart args — the world-delta watcher the unit would run.
    spec.args = vec!["world-delta".to_string(), "watch".to_string()];
    if let Some(u) = run_as {
        spec = spec.user(u);
    }
    spec
}

/// Render modgunn's world-delta service systemd `.service` unit for the given
/// exec/addr/scope — the one-call convenience `modgunn service --unit` uses (pure: no
/// root, no bus). Delegates to the ONE skidbladnir renderer via
/// [`ServiceSpec::render_systemd_unit`].
pub fn modgunn_service_unit(
    exec: &str,
    addr: &str,
    run_as: Option<&str>,
    user_scope: bool,
) -> String {
    modgunn_service_spec(exec, addr, run_as, user_scope).render_systemd_unit()
}

/// The service seam's headless oracle: the declared lifecycle as JSON (unit name ·
/// dedicated user · listen port · fat/thin posture · the shared renderer) — what
/// `modgunn service --json` dumps and a robot asserts.
pub fn state_json(
    exec: &str,
    addr: &str,
    run_as: Option<&str>,
    user_scope: bool,
) -> serde_json::Value {
    let spec = modgunn_service_spec(exec, addr, run_as, user_scope);
    serde_json::json!({
        "name": spec.name,
        "unit": format!("{}.service", spec.name),
        "user": spec.user,
        "port": addr_port(addr),
        "agent": if matches!(spec.agent, AgentMode::Fat) { "fat" } else { "thin" },
        "lifecycle": "skidbladnir::service::Lifecycle",
    })
}

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

    #[test]
    fn addr_port_parses_or_defaults() {
        assert_eq!(addr_port("127.0.0.1:50071"), 50071);
        assert_eq!(addr_port("0.0.0.0:6000"), 6000);
        assert_eq!(addr_port("garbage"), 50071);
    }

    /// **RED-when-broken: the system unit carries modgunn's identity + its DEDICATED
    /// `modgunn` system user, rendered by the SHARED ship renderer.** Asserts the
    /// ExecStart, the world-delta args, the listen-addr env, `User=modgunn` (its OWN
    /// user, per L5 — not a shared/blank one), on-failure restart, and the system-scope
    /// network ordering. If modgunn stopped declaring its service through the ship, or
    /// lost its dedicated user, this is red.
    #[test]
    fn system_unit_renders_dedicated_user_and_identity() {
        let unit = modgunn_service_unit(
            "/usr/local/bin/modgunn",
            DEFAULT_ADDR,
            Some(SERVICE_USER),
            false,
        );
        assert!(unit.contains("[Unit]"));
        assert!(unit
            .contains("Description=Móðguðr — the nordisk security + SCIP world-delta gatekeeper"));
        assert!(unit.contains("ExecStart=/usr/local/bin/modgunn"));
        assert!(
            unit.contains("world-delta"),
            "the declared world-delta watcher args:\n{unit}"
        );
        assert!(unit.contains("Environment=MODGUNN_WORLDDELTA_ADDR=127.0.0.1:50071"));
        assert!(
            unit.contains("User=modgunn"),
            "modgunn runs as its OWN dedicated user:\n{unit}"
        );
        assert!(unit.contains("Restart=on-failure"));
        assert!(unit.contains("After=network-online.target"));

        crate::functional_status(
            "modgunn/service",
            "system_unit_dedicated_user",
            unit.contains("User=modgunn") && unit.contains("ExecStart=/usr/local/bin/modgunn"),
            "modgunn declares its world-delta lifecycle through skidbladnir's shared renderer with a \
             dedicated `modgunn` system user (L5) — not a hand-rolled unit",
        );
    }

    /// A `--user` dev service runs as the caller: no `User=`, no system-target
    /// ordering — the same shape korp/nornir render through the ship.
    #[test]
    fn user_scope_unit_omits_user_and_network_ordering() {
        let unit = modgunn_service_unit("/home/me/.cargo/bin/modgunn", DEFAULT_ADDR, None, true);
        assert!(
            !unit.contains("User="),
            "no User= in a --user unit:\n{unit}"
        );
        assert!(
            !unit.contains("After="),
            "no system-target ordering in a --user unit"
        );
        assert!(unit.contains("WantedBy=default.target"));
    }

    /// The spec drives a real [`Lifecycle`]: it yields the `modgunn.service` unit name +
    /// unit text without touching D-Bus (pure), and its TCP health probe on a closed
    /// port reports UNHEALTHY (a real socket connect, not a stub).
    #[test]
    fn lifecycle_unit_name_and_closed_port_health() {
        let spec = modgunn_service_spec(
            "/usr/local/bin/modgunn",
            "127.0.0.1:1",
            Some(SERVICE_USER),
            false,
        );
        let lc = Lifecycle::new(spec);
        assert_eq!(lc.unit(), "modgunn.service");
        assert!(lc.unit_text().contains("ExecStart=/usr/local/bin/modgunn"));
        assert!(
            !lc.health().unwrap(),
            "closed-port TCP probe must be unhealthy"
        );
    }

    /// Health is TRUE when modgunn's declared port is actually listening — proving the
    /// ship's probe verifies the process is answering, not merely that systemd thinks
    /// the unit is active.
    #[test]
    fn lifecycle_health_true_when_port_listening() {
        use std::net::TcpListener;
        let l = TcpListener::bind("127.0.0.1:0").unwrap();
        let port = l.local_addr().unwrap().port();
        let spec = modgunn_service_spec(
            "/usr/local/bin/modgunn",
            &format!("127.0.0.1:{port}"),
            Some(SERVICE_USER),
            false,
        );
        let lc = Lifecycle::new(spec);
        assert!(lc.health().unwrap(), "an open port must probe healthy");
    }

    /// The `state_json` oracle carries the unit name, the dedicated user, the port, and
    /// the fat posture — the surface `modgunn service --json` dumps.
    #[test]
    fn state_json_carries_unit_user_and_posture() {
        let js = state_json(
            "/usr/local/bin/modgunn",
            DEFAULT_ADDR,
            Some(SERVICE_USER),
            false,
        );
        assert_eq!(js["unit"], "modgunn.service");
        assert_eq!(js["user"], "modgunn");
        assert_eq!(js["port"], 50071);
        assert_eq!(js["agent"], "fat");
    }
}