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.
75 lines
4.2 KiB
TypeScript
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)));
|