Files
mesh-catalog/modules/systemd/tools/index.ts
T
jochen 0596503db5 systemd: act as the runtime's account can, and never read a failure as an answer
The node tools runtime runs as the operator account, not root, and gives its bundles no
session words (novox/hq ADR 0175, 0188, 0193). So, per hq to-be 41 WP4:

- system-scope start/stop/restart/enable/disable go through sudo -n when not root, as the
  packet filter and intrusion prevention do, and a refusal is named by how it failed;
- user scope is plain --user with XDG_RUNTIME_DIR and the session bus of /run/user/<uid>;
  the dead --machine branches are gone;
- a failed systemctl or journalctl is an error, and an unreachable user manager is said
  even when systemctl exits 0; systemd_failed reports it beside the other manager's answer
  instead of claiming nothing failed;
- status says whether the mesh declares the unit: its loaded unit file begins with the
  header the host writes for a module's process. Only such a unit carries the restore note;
- the package resource goes: the service manager is always present, and it collided with
  systemd-networkd's identical declaration;
- calls are bounded below the runtime's call limit, a unit name is never an option, and
  the runner is injected so the tests use a fake one.
2026-10-04 03:58:19 +02:00

75 lines
4.2 KiB
TypeScript

// 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<Record<string, unknown>>): 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<Record<string, unknown>>): 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)));