UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

436 lines (435 loc) 19.8 kB
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 };