// systemd's tools: the node-service-manager seat's eight verbs — the units on this machine in // both scopes, read and acted on by name — and the module's own reading of what has failed // (novox/hq ADR 0177). The node tools runtime launches this bundle as a process of its own and // serves what it registers (ADR 0188, ADR 0193); it runs as the operator account, so acts on the // system manager escalate with sudo -n and the user scope is the account's own manager (client.ts). // The host applies units; this answers about them. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { ServiceManager, type Scope } from "../client.js"; const scope = { type: "string", description: "\"system\" (the default) or \"user\": the operator account's own manager" }; const unit = { type: "string", description: "the unit's name, as the service manager knows it" }; function scopeOf(args: Readonly>): Scope { const s = String(args.scope ?? "system"); if (s !== "system" && s !== "user") throw new Error(`scope ${JSON.stringify(s)}: "system" or "user"`); return s; } function unitOf(args: Readonly>): string { const u = String(args.unit ?? "").trim(); if (!u) throw new Error("a unit is required"); return u; } export function getSeatVerbs(manager: ServiceManager): ToolDefinition[] { const act = (verb: "start" | "stop" | "restart" | "enable" | "disable", description: string): ToolDefinition => ({ name: verb, description, input: { type: "object", properties: { scope, unit }, required: ["unit"] }, run: async (args) => manager.act(scopeOf(args), verb, unitOf(args)), }); return [ { name: "units", description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", input: { type: "object", properties: { scope, pattern: { type: "string", description: "a glob the unit's name must match (optional)" } } }, run: async (args) => ({ scope: scopeOf(args), units: await manager.units(scopeOf(args), args.pattern ? String(args.pattern) : undefined) }), }, { name: "status", description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and mesh_declared — true when its unit file is one the mesh wrote (a unit the mesh only puts into a state is not recognised from here).", input: { type: "object", properties: { scope, unit }, required: ["unit"] }, run: async (args) => manager.status(scopeOf(args), unitOf(args)), }, act("start", "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at its next apply."), act("stop", "Stop one unit; for a unit the mesh declares, the answer says the host will restore its declared state."), act("restart", "Restart one unit."), act("enable", "Make one unit start at boot (or at the account's login, in user scope)."), act("disable", "Stop one unit starting at boot (or at login, in user scope)."), { name: "journal", description: "The last lines of one unit's journal (at most 2000).", input: { type: "object", properties: { scope, unit, lines: { type: "number", description: "how many lines from the end (default 100, at most 2000)" } }, required: ["unit"] }, run: async (args) => { // Bounded so the answer stays well below what the runtime carries back in one reply. const n = Math.floor(Number(args.lines ?? 100)); return manager.journal(scopeOf(args), unitOf(args), Number.isFinite(n) && n > 0 ? Math.min(n, 2000) : 100); }, }, ]; } export function getOwnTools(manager: ServiceManager): ToolDefinition[] { return [ { name: "systemd_failed", description: "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, not as nothing failed.", input: { type: "object", properties: {} }, run: async () => manager.failed(), }, ]; } registerModuleTools("node-service-manager", (env) => getSeatVerbs(ServiceManager.fromEnv(env))); registerModuleTools("systemd", (env) => getOwnTools(ServiceManager.fromEnv(env)));