#!/usr/bin/env node /** * @file systemd-system-service.js * @description Server mode with one Linux user per account (S3): the dashboard * runs as a SYSTEM unit, `/etc/systemd/system/agenthub.service`, written once * by `agenthub server install-system` (scripts/server/install-system.sh). * The unit runs as the host account with group `agenthub` and * CAP_DAC_READ_SEARCH — the two things a `systemd --user` unit cannot give. * * Same API as `systemd-service.js`, so the bootstrap, Quit, the autostart * switch and the doctor keep their rules unchanged (`service-manager.js` * picks this module while that unit exists and is ours). The difference is who * may write what: * - the unit file is root's and never rewritten here. It reads the plugin * root, node and env from `/agenthub.system.env`, a file the * HOST owns — so `writeUserService` (the bootstrap's repoint after a plugin * update) is a plain file write, no root; * - start, stop, restart, enable and disable go through * `sudo -n systemctl …`, allowed for exactly those commands by the * sudoers file the installer wrote. `--no-block` everywhere it matters, * for the same cgroup reason as the user unit (CLAUDE.md). * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("fs"); const path = require("path"); const { spawnSync } = require("child_process"); const MARKER = "# agenthub-managed-system-unit"; const UNIT_DIR = "/etc/systemd/system"; function unitPath(name) { return path.join(process.env.AGENTHUB_SYSTEM_UNIT_DIR || UNIT_DIR, `${name}.service`); } /** The host-owned env file the unit's `EnvironmentFile=` names. */ function envPath() { const { getDataDir } = require("../server/lib/claude-home"); return path.join(getDataDir(), "runtime", "agenthub.system.env"); } // Test seams: `systemctl` itself (reads) and `sudo -n systemctl` (writes). const defaultExec = (args, opts) => spawnSync("systemctl", args, opts); const defaultSudo = (args, opts) => spawnSync("sudo", ["-n", "systemctl", ...args], opts); let execImpl = defaultExec; let sudoImpl = defaultSudo; function __setExecImpl(fn, sudoFn) { execImpl = fn; sudoImpl = sudoFn || fn; } function __reset() { execImpl = defaultExec; sudoImpl = defaultSudo; } /** Is there a system unit by this name, written by the installer? Pure fs. */ function unitIsOurs(name) { try { return fs.readFileSync(unitPath(name), "utf8").includes(MARKER); } catch { return false; } } function systemctlAvailable() { if (process.platform !== "linux" || !unitIsOurs("agenthub")) return false; return execImpl(["--version"], { stdio: "ignore" }).status === 0; } function readEnv() { try { return Object.fromEntries( fs .readFileSync(envPath(), "utf8") .split("\n") .map((l) => /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(l)) .filter(Boolean) .map((m) => [m[1], m[2]]) ); } catch { return null; } } /** The plugin root the unit runs from (env file), or null. Pure fs. */ function unitRoot(name) { if (!fs.existsSync(unitPath(name))) return null; return readEnv()?.AGENTHUB_ROOT || null; } /** Does the env file still name `root`? Pure fs, like the user unit's. */ function unitIsStale(name, root) { if (!unitIsOurs(name)) return false; return unitRoot(name) !== root; } function unitIsEnabled(name) { if (!systemctlAvailable()) return false; const res = execImpl(["is-enabled", `${name}.service`], { encoding: "utf8", stdio: "pipe" }); return String(res.stdout || "").trim() === "enabled"; } function unitIsActive(name) { if (!systemctlAvailable()) return false; const res = execImpl(["is-active", `${name}.service`], { encoding: "utf8", stdio: "pipe" }); return String(res.stdout || "").trim() === "active"; } function setUnitEnabled(name, on) { if (!systemctlAvailable()) return "unavailable"; const res = sudoImpl([on ? "enable" : "disable", `${name}.service`], { stdio: "ignore" }); return res.status === 0 ? "ok" : "failed"; } /** * Point the unit at this plugin version: rewrite the host-owned env file the * unit reads. Starts nothing, enables nothing (the unit file is root's). * @returns {"written"|"unchanged"|"unavailable"|"failed"} */ function writeUserService({ argv, env = {} }) { if (!systemctlAvailable()) return "unavailable"; const root = path.dirname(path.dirname(argv[1])); // /server/index.js // The port install-system chose outlives a bootstrap that does not pass one // (server.conf and the member hook still name it). // ponytail: only DASHBOARD_PORT is carried over; others if they ever matter. let kept = {}; try { const m = /^DASHBOARD_PORT=(\d+)$/m.exec(fs.readFileSync(envPath(), "utf8")); if (m) kept = { DASHBOARD_PORT: m[1] }; } catch { /* no file yet */ } const body = Object.entries({ AGENTHUB_NODE: argv[0], AGENTHUB_ROOT: root, ...kept, ...env }) .map(([k, v]) => `${k}=${String(v).replace(/\n/g, "")}`) .join("\n") + "\n"; try { const file = envPath(); if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === body) return "unchanged"; fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(`${file}.tmp`, body, { mode: 0o600 }); fs.renameSync(`${file}.tmp`, file); return "written"; } catch { return "failed"; } } /** `--no-block`: the caller may sit in this unit's cgroup (CLAUDE.md). */ function restartUserService(name) { if (!systemctlAvailable()) return "unavailable"; const res = sudoImpl(["restart", "--no-block", `${name}.service`], { stdio: "ignore" }); return res.status === 0 ? "restarted" : "failed"; } /** Point the unit here, then start it (without blocking). */ function installUserService(opts) { const result = writeUserService(opts); if (result === "written" || result === "unchanged") { sudoImpl(["start", "--no-block", `${opts.name}.service`], { stdio: "ignore" }); } return result; } /** Removing the system unit is root's: `agenthub server install-system --uninstall`. */ function uninstallUserService() { return "foreign"; } /** A system unit starts at boot; there is no linger to enable. */ function enableLinger() { return true; } /** The argv that stops the unit without waiting (Quit, server/lib/quit.js). */ function stopArgv(name) { return ["sudo", ["-n", "systemctl", "stop", "--no-block", `${name}.service`]]; } module.exports = { kind: "systemd-system", label: "systemd system service", MARKER, systemctlAvailable, unitIsOurs, unitIsStale, unitRoot, unitIsActive, unitIsEnabled, setUnitEnabled, writeUserService, restartUserService, installUserService, uninstallUserService, enableLinger, stopArgv, unitPath, envPath, __setExecImpl, __reset, };