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");
}
}