openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
436 lines (435 loc) • 19.8 kB
JavaScript
import { n as isNodeRuntime, t as isBunRuntime } from "./runtime-binary-Cy5Lhult.js";
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { n as parseTcpPort } from "./tcp-port-C3gLZtJi.js";
import { n as POSIX_SHELL_WRAPPERS, x as resolveInlineCommandMatch } from "./shell-wrapper-resolution-CIQIf-gP.js";
import { o as resolveSystemdServiceName, s as resolveSystemdUnitPath } from "./systemd-service-files-BCcumv4T.js";
import { n as SUPPORTED_NODE_VERSIONS } from "./node-version-WVgs0-1c.js";
import { n as collectInlineManagedServiceEnvKeys, o as hasInlineEnvironmentSource, r as collectInlineServiceEnvKeys, s as isEnvironmentFileOnlySource, u as readEnvironmentValueSource } from "./service-managed-env-j4Dxj-Tf.js";
import { c as resolveSystemNodePath, d as SERVICE_PROXY_ENV_KEYS, i as resolveBunRuntimeInfo, l as isNonMinimalServicePathEntry, m as getMinimalServicePathPartsFromEnv, n as isVersionManagedNodePath, t as isSystemNodePath, u as normalizeServicePathEntry } from "./runtime-paths-BjNV6RBz.js";
import "./launchd-BH9PtJsR.js";
import { o as resolveLaunchAgentPlistPath } from "./launchd-service-files-ook2RU8t.js";
import { t as parseKeyValueOutput } from "./runtime-parse-DmkaBdRh.js";
import { a as execSystemctlUser } from "./systemd-exec-C9fneC4I.js";
import { a as splitSystemdLogicalLines, n as parseSystemdEnvAssignments } from "./systemd-unit-CP-8WLG3.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/daemon/service-audit.ts
/** Audits installed daemon service definitions for drift and repair candidates. */
const SERVICE_AUDIT_CODES = {
gatewayCommandMissing: "gateway-command-missing",
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
gatewayPathMissing: "gateway-path-missing",
gatewayPathMissingDirs: "gateway-path-missing-dirs",
gatewayPathNonMinimal: "gateway-path-nonminimal",
gatewayTokenEmbedded: "gateway-token-embedded",
gatewayPasswordEmbedded: "gateway-password-embedded",
gatewayManagedEnvEmbedded: "gateway-managed-env-embedded",
gatewayPortMismatch: "gateway-port-mismatch",
gatewayProxyEnvEmbedded: "gateway-proxy-env-embedded",
gatewayTokenMismatch: "gateway-token-mismatch",
gatewayRuntimeBun: "gateway-runtime-bun",
gatewayRuntimeProbeFailed: "gateway-runtime-probe-failed",
gatewayRuntimeNodeVersionManager: "gateway-runtime-node-version-manager",
gatewayRuntimeNodeSystemMissing: "gateway-runtime-node-system-missing",
gatewayTokenDrift: "gateway-token-drift",
launchdKeepAlive: "launchd-keep-alive",
launchdRunAtLoad: "launchd-run-at-load",
systemdAfterNetworkOnline: "systemd-after-network-online",
systemdRestartSec: "systemd-restart-sec",
systemdWantsNetworkOnline: "systemd-wants-network-online",
systemdKillModeProcessOrNone: "systemd-kill-mode-process-or-none",
systemdUnitBackupUnsafe: "systemd-unit-backup-unsafe"
};
/** Returns whether audit issues require migrating a daemon to a stable Node runtime. */
function needsNodeRuntimeMigration(issues) {
return issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun || issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager);
}
function hasGatewaySubcommand(programArguments) {
return Boolean(programArguments?.some((arg) => arg === "gateway"));
}
const POSIX_SERVICE_INLINE_COMMAND_FLAGS = /* @__PURE__ */ new Set(["-c"]);
const POSIX_SERVICE_SHELL_WRAPPERS = POSIX_SHELL_WRAPPERS;
const SYSTEMD_AUDIT_TIMEOUT_MS = 1e4;
function isOpaquePosixShellInlineCommand(programArguments) {
const executable = programArguments[0]?.trim();
const shellName = executable ? path.posix.basename(executable).toLowerCase() : "";
if (!POSIX_SERVICE_SHELL_WRAPPERS.has(shellName)) return false;
return resolveInlineCommandMatch(programArguments, POSIX_SERVICE_INLINE_COMMAND_FLAGS, { allowCombinedC: true }).command !== null;
}
function parseSystemdUnit(content) {
const after = /* @__PURE__ */ new Set();
const wants = /* @__PURE__ */ new Set();
let restartSec;
let killMode;
for (const rawLine of splitSystemdLogicalLines(content)) {
const line = rawLine.trim();
if (!line) continue;
if (line.startsWith("#") || line.startsWith(";")) continue;
if (line.startsWith("[")) continue;
const idx = line.indexOf("=");
if (idx <= 0) continue;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (!value) continue;
if (key === "After") {
for (const entry of value.split(/\s+/)) if (entry) after.add(entry);
} else if (key === "Wants") {
for (const entry of value.split(/\s+/)) if (entry) wants.add(entry);
} else if (key === "RestartSec") restartSec = value;
else if (key === "KillMode") killMode = value;
}
return {
after,
wants,
restartSec,
killMode
};
}
function isRestartSecPreferred(value) {
if (!value) return false;
const parsed = parseSystemdRestartSecSeconds(value);
if (parsed === void 0) return false;
return Math.abs(parsed - 5) < .01;
}
function parseSystemdRestartSecSeconds(value) {
const match = value.trim().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(?:\s*(?:s|sec|secs|second|seconds))?$/iu);
if (!match) return;
const parsed = Number(match[1]);
return Number.isFinite(parsed) ? parsed : void 0;
}
async function auditSystemdUnit(env, issues, timeoutMs) {
const unitPath = resolveSystemdUnitPath(env);
await auditSystemdUnitBackup(unitPath, issues);
let content;
try {
content = await fs.readFile(unitPath, "utf8");
} catch {
return;
}
const manager = await execSystemctlUser(env, [
"show",
`${resolveSystemdServiceName(env)}.service`,
"--no-page",
"--property",
"After,Wants,RestartUSec,KillMode"
], timeoutMs && timeoutMs > 0 ? timeoutMs : SYSTEMD_AUDIT_TIMEOUT_MS);
const entries = manager.code === 0 ? parseKeyValueOutput(manager.stdout, "=") : void 0;
const parsed = entries ? {
after: new Set(entries.after?.split(/\s+/).filter(Boolean)),
wants: new Set(entries.wants?.split(/\s+/).filter(Boolean)),
restartSec: entries.restartusec,
killMode: entries.killmode
} : parseSystemdUnit(content);
if (!parsed.after.has("network-online.target")) issues.push({
code: SERVICE_AUDIT_CODES.systemdAfterNetworkOnline,
message: "Missing systemd After=network-online.target",
detail: unitPath,
level: "recommended"
});
if (!parsed.wants.has("network-online.target")) issues.push({
code: SERVICE_AUDIT_CODES.systemdWantsNetworkOnline,
message: "Missing systemd Wants=network-online.target",
detail: unitPath,
level: "recommended"
});
if (!isRestartSecPreferred(parsed.restartSec)) issues.push({
code: SERVICE_AUDIT_CODES.systemdRestartSec,
message: "RestartSec does not match the recommended 5s",
detail: unitPath,
level: "recommended"
});
const killMode = normalizeLowercaseStringOrEmpty(parsed.killMode);
if (killMode === "process" || killMode === "none") issues.push({
code: SERVICE_AUDIT_CODES.systemdKillModeProcessOrNone,
message: "KillMode is process/none; service child processes can survive gateway stops and restarts.",
detail: `${unitPath}: ${killMode}`,
level: "recommended"
});
}
async function auditSystemdUnitBackup(unitPath, issues) {
const backupPath = `${unitPath}.bak`;
let stat;
try {
stat = await fs.lstat(backupPath);
} catch {
return;
}
const mode = stat.mode & 511;
const embeddedKeys = /* @__PURE__ */ new Set();
let unreadable = false;
if (stat.isFile()) {
const content = await fs.readFile(backupPath, "utf8").catch(() => {
unreadable = true;
return "";
});
for (const rawLine of splitSystemdLogicalLines(content)) {
const line = rawLine.trim();
const separator = line.indexOf("=");
if (separator < 0 || line.slice(0, separator).trim() !== "Environment") continue;
for (const { key, value } of parseSystemdEnvAssignments(line.slice(separator + 1).trim())) {
const normalizedKey = key.toUpperCase();
if (value && (normalizedKey === "OPENCLAW_GATEWAY_TOKEN" || normalizedKey === "OPENCLAW_GATEWAY_PASSWORD")) embeddedKeys.add(normalizedKey);
}
}
}
if (stat.isFile() && !unreadable && embeddedKeys.size === 0 && (mode & 63) === 0) return;
const detail = [
backupPath,
!stat.isFile() ? "not a regular file" : void 0,
unreadable ? "unreadable" : void 0,
embeddedKeys.size > 0 ? `embedded keys: ${[...embeddedKeys].toSorted().join(", ")}` : void 0,
(mode & 63) !== 0 ? `mode: ${mode.toString(8).padStart(3, "0")}` : void 0
].filter(Boolean).join("; ");
issues.push({
code: SERVICE_AUDIT_CODES.systemdUnitBackupUnsafe,
message: embeddedKeys.size > 0 ? "Systemd service backup exposes gateway credentials; reinstall the service and rotate the embedded credentials." : "Systemd service backup is unsafe; reinstall the service to replace it.",
detail,
level: "recommended"
});
}
async function auditLaunchdPlist(env, issues) {
const plistPath = resolveLaunchAgentPlistPath(env);
let content;
try {
content = await fs.readFile(plistPath, "utf8");
} catch {
return;
}
const hasRunAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/i.test(content);
const hasKeepAlive = /<key>KeepAlive<\/key>\s*<true\s*\/>/i.test(content);
if (!hasRunAtLoad) issues.push({
code: SERVICE_AUDIT_CODES.launchdRunAtLoad,
message: "LaunchAgent is missing RunAtLoad=true",
detail: plistPath,
level: "recommended"
});
if (!hasKeepAlive) issues.push({
code: SERVICE_AUDIT_CODES.launchdKeepAlive,
message: "LaunchAgent is missing KeepAlive=true",
detail: plistPath,
level: "recommended"
});
}
function auditGatewayCommand(programArguments, issues) {
if (!programArguments || programArguments.length === 0) return;
if (!hasGatewaySubcommand(programArguments) && !isOpaquePosixShellInlineCommand(programArguments)) issues.push({
code: SERVICE_AUDIT_CODES.gatewayCommandMissing,
message: "Service command does not include the gateway subcommand",
level: "aggressive"
});
}
function parseGatewayPortArg(value) {
const raw = value?.trim() ?? "";
const port = parseTcpPort(raw);
if (port !== null) return {
kind: "valid",
port
};
return raw ? {
kind: "invalid",
raw
} : { kind: "missing" };
}
function readGatewayServiceCommandPortState(programArguments) {
if (!programArguments || programArguments.length === 0) return { kind: "missing" };
let latest = { kind: "missing" };
for (let index = 0; index < programArguments.length; index += 1) {
const arg = programArguments[index];
if (arg === "--port") {
latest = parseGatewayPortArg(programArguments[index + 1]);
index += 1;
continue;
}
if (arg?.startsWith("--port=")) latest = parseGatewayPortArg(arg.slice(7));
}
return latest;
}
function auditGatewayServicePort(params) {
if (typeof params.expectedPort !== "number" || !Number.isSafeInteger(params.expectedPort) || params.expectedPort <= 0 || params.expectedPort > 65535) return;
const servicePort = readGatewayServiceCommandPortState(params.programArguments);
if (servicePort.kind === "missing") return;
if (servicePort.kind === "valid" && servicePort.port === params.expectedPort) return;
const detail = servicePort.kind === "valid" ? `${servicePort.port} -> ${params.expectedPort}` : `${servicePort.raw} -> ${params.expectedPort}`;
params.issues.push({
code: SERVICE_AUDIT_CODES.gatewayPortMismatch,
message: "Gateway service port does not match current gateway config.",
detail,
level: "recommended"
});
}
function auditGatewayToken(command, issues, expectedGatewayToken) {
const serviceToken = readEmbeddedGatewayToken(command);
if (!serviceToken) return;
issues.push({
code: SERVICE_AUDIT_CODES.gatewayTokenEmbedded,
message: "Gateway service embeds OPENCLAW_GATEWAY_TOKEN and should be reinstalled.",
level: "recommended"
});
const expectedToken = normalizeOptionalString(expectedGatewayToken);
if (!expectedToken || serviceToken === expectedToken) return;
issues.push({
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
message: "Gateway service OPENCLAW_GATEWAY_TOKEN does not match gateway.auth.token in openclaw.json",
detail: "service token is stale",
level: "recommended"
});
}
function auditGatewayPassword(command, issues) {
if (!command?.environment?.OPENCLAW_GATEWAY_PASSWORD?.trim() || isEnvironmentFileOnlySource(command.environmentValueSources?.OPENCLAW_GATEWAY_PASSWORD)) return;
issues.push({
code: SERVICE_AUDIT_CODES.gatewayPasswordEmbedded,
message: "Gateway service embeds OPENCLAW_GATEWAY_PASSWORD and should be reinstalled.",
detail: "Rotate the password after reinstalling because the service definition exposed it.",
level: "recommended"
});
}
function auditManagedServiceEnvironment(command, issues, expectedManagedServiceEnvKeys) {
const inlineKeys = collectInlineManagedServiceEnvKeys(command, expectedManagedServiceEnvKeys);
if (inlineKeys.length === 0) return;
issues.push({
code: SERVICE_AUDIT_CODES.gatewayManagedEnvEmbedded,
message: "Gateway service embeds managed environment values that should load at runtime.",
detail: `inline keys: ${inlineKeys.join(", ")}`,
environmentKeys: inlineKeys,
level: "recommended"
});
}
function auditProxyServiceEnvironment(command, issues) {
const inlineKeys = collectInlineServiceEnvKeys(command, SERVICE_PROXY_ENV_KEYS);
if (inlineKeys.length === 0) return;
issues.push({
code: SERVICE_AUDIT_CODES.gatewayProxyEnvEmbedded,
message: "Gateway service embeds proxy environment values that should not be persisted.",
detail: `inline keys: ${inlineKeys.join(", ")}`,
environmentKeys: Object.entries(command?.environment ?? {}).filter(([key, value]) => value.trim() && SERVICE_PROXY_ENV_KEYS.some((proxyKey) => proxyKey === key) && hasInlineEnvironmentSource(readEnvironmentValueSource(command?.environmentValueSources, key))).map(([key]) => key).toSorted(),
level: "recommended"
});
}
function readEmbeddedGatewayToken(command) {
if (!command) return;
if (isEnvironmentFileOnlySource(command.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN)) return;
return normalizeOptionalString(command.environment?.OPENCLAW_GATEWAY_TOKEN);
}
function getEquivalentMinimalPathEntries(entry, platform, normalizedExpected) {
if (platform !== "linux") return [];
const equivalent = entry.endsWith("/aliases/default/bin") ? `${entry.slice(0, -20)}/current/bin` : entry.endsWith("/current/bin") ? `${entry.slice(0, -12)}/aliases/default/bin` : void 0;
if (!equivalent) return [];
const normalizedEquivalent = normalizeServicePathEntry(equivalent, platform);
return normalizedExpected.has(normalizedEquivalent) ? [equivalent] : [];
}
function auditGatewayServicePath(command, issues, env, platform, expectedServicePath) {
if (!command) return;
if (platform === "win32") return;
const servicePath = command?.environment?.PATH;
if (!servicePath) {
issues.push({
code: SERVICE_AUDIT_CODES.gatewayPathMissing,
message: "Gateway service PATH is not set; the daemon should use a minimal PATH.",
level: "recommended"
});
return;
}
const expected = expectedServicePath?.trim() ? normalizeStringEntries(expectedServicePath.split(path.posix.delimiter)) : getMinimalServicePathPartsFromEnv({
platform,
env,
includeMissingUserBinDefaults: false
});
const parts = normalizeStringEntries(servicePath.split(path.posix.delimiter));
const normalizedParts = new Set(parts.map((entry) => normalizeServicePathEntry(entry, platform)));
const normalizedExpected = new Set(expected.map((entry) => normalizeServicePathEntry(entry, platform)));
const missing = expected.filter((entry) => {
const normalized = normalizeServicePathEntry(entry, platform);
if (normalizedParts.has(normalized)) return false;
return !getEquivalentMinimalPathEntries(entry, platform, normalizedExpected).some((equivalent) => normalizedParts.has(normalizeServicePathEntry(equivalent, platform)));
});
if (missing.length > 0) issues.push({
code: SERVICE_AUDIT_CODES.gatewayPathMissingDirs,
message: `Gateway service PATH missing required dirs: ${missing.join(", ")}`,
level: "recommended"
});
const nonMinimal = parts.filter((entry) => {
const normalized = normalizeServicePathEntry(entry, platform);
if (normalizedExpected.has(normalized)) return false;
return isNonMinimalServicePathEntry(normalized, platform);
});
if (nonMinimal.length > 0) issues.push({
code: SERVICE_AUDIT_CODES.gatewayPathNonMinimal,
message: "Gateway service PATH includes version managers or package managers; recommend a minimal PATH.",
detail: nonMinimal.join(", "),
level: "recommended"
});
}
async function auditGatewayRuntime(env, command, issues, platform) {
const execPath = command?.programArguments?.[0];
if (!execPath) return;
if (isBunRuntime(execPath)) {
const runtime = await resolveBunRuntimeInfo(execPath);
if (runtime.status !== "supported") issues.push({
code: runtime.status === "probe-failed" ? SERVICE_AUDIT_CODES.gatewayRuntimeProbeFailed : SERVICE_AUDIT_CODES.gatewayRuntimeBun,
message: runtime.status === "probe-failed" ? "Gateway service Bun runtime probe failed." : "Gateway service uses an unsupported Bun runtime; Bun 1.4+ with WAL-reset-safe node:sqlite is required.",
detail: runtime.status === "probe-failed" ? runtime.error.message : execPath,
level: "recommended"
});
return;
}
if (!isNodeRuntime(execPath)) return;
if (isVersionManagedNodePath(execPath, platform)) {
issues.push({
code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager,
message: "Gateway service uses Node from a version manager; it can break after upgrades.",
detail: execPath,
level: "recommended"
});
if (!isSystemNodePath(execPath, env, platform)) {
if (!await resolveSystemNodePath(env, platform)) issues.push({
code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeSystemMissing,
message: `System Node ${SUPPORTED_NODE_VERSIONS} not found; install it before migrating away from version managers.`,
level: "recommended"
});
}
}
}
/**
* Check if the service's embedded token differs from the config file token.
* Returns an issue if drift is detected (service will use old token after restart).
* The invoking CLI selects recovery advice for its installation.
*/
function checkTokenDrift(params) {
const serviceToken = normalizeOptionalString(params.serviceToken);
const configToken = normalizeOptionalString(params.configToken);
if (!serviceToken) return null;
if (configToken && serviceToken !== configToken) return {
code: SERVICE_AUDIT_CODES.gatewayTokenDrift,
message: "Config token differs from service token. The daemon will use the old token after restart.",
level: "recommended"
};
return null;
}
async function auditGatewayServiceConfig(params) {
const issues = [];
const platform = params.platform ?? process.platform;
auditGatewayCommand(params.command?.programArguments, issues);
auditGatewayServicePort({
programArguments: params.command?.programArguments,
issues,
expectedPort: params.expectedPort
});
auditManagedServiceEnvironment(params.command, issues, params.expectedManagedServiceEnvKeys);
auditProxyServiceEnvironment(params.command, issues);
auditGatewayToken(params.command, issues, params.expectedGatewayToken);
auditGatewayPassword(params.command, issues);
auditGatewayServicePath(params.command, issues, params.env, platform, params.expectedServicePath);
await auditGatewayRuntime(params.env, params.command, issues, platform);
if (platform === "linux") await auditSystemdUnit(params.env, issues, params.timeoutMs);
else if (platform === "darwin") await auditLaunchdPlist(params.env, issues);
return issues.length === 0 ? {
ok: true,
issues
} : {
ok: false,
issues
};
}
//#endregion
export { readEmbeddedGatewayToken as a, needsNodeRuntimeMigration as i, auditGatewayServiceConfig as n, checkTokenDrift as r, SERVICE_AUDIT_CODES as t };