UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

251 lines (250 loc) • 12 kB
import { _ as resolveNodeLaunchAgentLabel, u as resolveGatewayLaunchAgentLabel } from "./constants-BLghGsId.mjs"; import { n as sanitizeForLog } from "./ansi-BeFJ0Uu7.mjs"; import { n as resolveLaunchAgentLabel } from "./launchd-label-CDVrpZuO.mjs"; import { b as isLaunchctlNotLoaded, v as execLaunchctl, y as formatLaunchctlResultDetail } from "./launchd-service-files-UOyeu4f-.mjs"; import { g as resolveLaunchAgentGuiDomain, l as parseLaunchctlJob } from "./launchd-runtime-CkSyqekH.mjs"; import { constants } from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; //#region src/daemon/launchd-foreign-jobs.ts /** Live launchd diagnostics and narrowly scoped Doctor repair for auxiliary jobs. */ const MAX_JOBS = 64; const INSPECTION_TIMEOUT_MS = 2e3; const MAX_FILE_BYTES = 65536; const SHELLS = /* @__PURE__ */ new Set([ "sh", "bash", "zsh", "dash", "ksh" ]); const SHELL_EXECUTION_ENV_NAMES = /* @__PURE__ */ new Set([ "SHELLOPTS", "BASHOPTS", "BASH_ENV", "ENV", "ZDOTDIR", "POSIXLY_CORRECT", "IFS", "CDPATH", "PS4", "BASH_XTRACEFD" ]); function hasShellExecutionEnvironment(environment) { for (const [, name] of environment.matchAll(/^\t\t(.+?) => /gm)) if (name && (SHELL_EXECUTION_ENV_NAMES.has(name) || name.startsWith("BASH_"))) return true; return false; } function isCandidate(label, env) { return /^ai\.openclaw\.[A-Za-z0-9._-]+$/.test(label) && !(/* @__PURE__ */ new Set([ resolveGatewayLaunchAgentLabel(), resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE), resolveLaunchAgentLabel(env), resolveNodeLaunchAgentLabel() ])).has(label); } function lifecycleInvocation(args) { if (args.some((arg) => [ "--help", "-h", "--version", "-V" ].includes(arg))) return; let words = args; if (path.basename(words[0] ?? "") === "env") { words = words.slice(1); while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? "")) words = words.slice(1); } const program = words[0] ?? ""; if (!path.isAbsolute(program)) return; if (["node", "bun"].includes(path.basename(words[0] ?? ""))) { words = words.slice(1); if (path.basename(words[0] ?? "") !== "openclaw.mjs") return; } if (!path.isAbsolute(words[0] ?? "") || !["openclaw", "openclaw.mjs"].includes(path.basename(words[0] ?? ""))) return; words = words.slice(1); if (words[0] === "--profile" && words[1]) words = words.slice(2); const action = words[1]; return words[0] === "gateway" && (action === "restart" || action === "start" || action === "stop") ? { action, program } : void 0; } const SCRIPT_HELPER_NAME = /(?:openclaw_|OPENCLAW_)[A-Za-z0-9_]*/; const SCRIPT_LITERAL_EXEC = String.raw`/(?:[A-Za-z0-9_.-]+/)*openclaw(?:\.mjs)?`; const SCRIPT_HELPER_REF = String.raw`\$(?:${SCRIPT_HELPER_NAME.source}|\{${SCRIPT_HELPER_NAME.source}\})`; const SCRIPT_LINE_END = String.raw`$(?![\s\S])`; const SCRIPT_EXECUTABLE = new RegExp(`^${SCRIPT_LITERAL_EXEC}${SCRIPT_LINE_END}`); const SCRIPT_ASSIGNMENT = new RegExp(String.raw`^[ \t]*(?:export[ \t]+)?(${SCRIPT_HELPER_NAME.source})=([A-Za-z0-9_./-]+)[ \t]*${SCRIPT_LINE_END}`); const SCRIPT_INVOCATION = new RegExp(String.raw`^[ \t]*(?:exec[ \t]+)?(${SCRIPT_LITERAL_EXEC}|${SCRIPT_HELPER_REF}|"${SCRIPT_HELPER_REF}")[ \t]+gateway[ \t]+(restart|start|stop)((?:[ \t]+[A-Za-z0-9_.=/:-]+)*)[ \t]*${SCRIPT_LINE_END}`); function scriptActions(script) { if (script.includes("\r") || script.includes("<<") || script.includes("\\\n")) return []; const variables = /* @__PURE__ */ new Map(); for (const line of script.split("\n")) { if (/^[ \t]*(?:#.*)?$(?![\s\S])/.test(line)) continue; if (/^[ \t]*set(?:[ \t]+[+-][eux])+[ \t]*$(?![\s\S])/.test(line) || /^[ \t]*set[ \t]+-o[ \t]+pipefail[ \t]*$(?![\s\S])/.test(line)) continue; const assignment = SCRIPT_ASSIGNMENT.exec(line); if (assignment?.[1] && assignment[2]) { variables.set(assignment[1], assignment[2]); continue; } const invocation = SCRIPT_INVOCATION.exec(line); const executable = invocation?.[1]; const action = invocation?.[2]; if (!executable || action !== "restart" && action !== "start" && action !== "stop") return []; if (!SCRIPT_EXECUTABLE.test(executable)) { const name = executable.match(SCRIPT_HELPER_NAME)?.[0]; const resolved = name && variables.get(name); if (!resolved || !SCRIPT_EXECUTABLE.test(resolved)) return []; } if (/(?:^|[ \t])(?:-h|--help|-V|--version)(?:[ \t]|$)/.test(invocation?.[3] ?? "")) return []; return [action]; } return []; } async function readShellScript(args) { for (const [index, arg] of args.entries()) { if (arg === "--") return await readOwnedText(args[index + 1] ?? ""); if (!arg.startsWith("-")) return await readOwnedText(arg); if (/^-[elux]*c[elux]*$/.test(arg)) return args[index + 1]; if (!/^-[elux]+$/.test(arg)) return; } } function hasShellShebang(script) { const firstLine = script.split(/\r?\n/, 1)[0] ?? ""; if (!firstLine.startsWith("#!")) return false; const [interpreter = "", ...options] = firstLine.slice(2).trim().split(/\s+/); if (interpreter === "/usr/bin/env") return options.length === 1 && SHELLS.has(options[0] ?? ""); return path.isAbsolute(interpreter) && SHELLS.has(path.basename(interpreter)) && (options.length === 0 || options.length === 1 && /^-[elux]+$/.test(options[0] ?? "")); } async function readOwnedText(filePath) { if (!path.isAbsolute(filePath)) return; const file = await fs$1.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => null); if (!file) return; try { const stat = await file.stat(); if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.uid !== process.getuid?.()) return; const buffer = Buffer.alloc(65537); const { bytesRead } = await file.read(buffer, 0, buffer.length, 0); return bytesRead <= MAX_FILE_BYTES ? buffer.subarray(0, bytesRead).toString("utf8") : void 0; } finally { await file.close(); } } async function inspectJob(label, env) { if (!isCandidate(label, env)) return null; const target = `${resolveLaunchAgentGuiDomain()}/${label}`; const result = await execLaunchctl(["print", target], INSPECTION_TIMEOUT_MS); if (isLaunchctlNotLoaded(result)) return null; if (result.code !== 0) throw new Error(`Cannot inspect launchd job ${label}: ${formatLaunchctlResultDetail(result)}`); const job = parseLaunchctlJob(result.stdout, target); const field = (name) => job.fields.get(name); const program = field("program") ?? "unknown"; const rawPath = field("path"); const plistPath = rawPath?.startsWith("/") ? rawPath : void 0; const args = job.arguments ?? []; const environment = job.environment; const environmentBlocks = job.environmentBlocks; const plist = plistPath ? await readOwnedText(plistPath) : void 0; const hasServiceMarker = /^\t\tOPENCLAW_SERVICE_MARKER => openclaw$/m.test(environment) && /^\t\tOPENCLAW_SERVICE_KIND => (gateway|node)$/m.test(environment); const generatedWrapper = args.some((arg) => arg.endsWith(`/service-env/${label}-env-wrapper.sh`)); if (hasServiceMarker || generatedWrapper || /<key>Comment<\/key>\s*<string>OpenClaw (Gateway|Node)\b/.test(plist ?? "")) return null; let actions = []; let diagnostic; const shellEnvironmentDiagnostic = hasShellExecutionEnvironment(environmentBlocks) ? "Shell environment alters execution; left unchanged." : void 0; const command = args.length ? [program, ...args.slice(1)] : [program]; const direct = path.isAbsolute(program) ? lifecycleInvocation(command) : void 0; const programName = path.basename(program); const isShell = SHELLS.has(programName); const programScript = !isShell && (shellEnvironmentDiagnostic || ![ "openclaw", "openclaw.mjs", "node", "bun", "env" ].includes(programName)) ? await readOwnedText(program) : void 0; let isShellScript = programScript !== void 0 && hasShellShebang(programScript); if (shellEnvironmentDiagnostic && direct && direct.program !== program) { const selectedScript = await readOwnedText(direct.program); isShellScript ||= selectedScript !== void 0 && hasShellShebang(selectedScript); } if (shellEnvironmentDiagnostic && (isShell || isShellScript)) diagnostic = shellEnvironmentDiagnostic; else if (direct) actions = [direct.action]; else if (isShell) { const script = await readShellScript(command.slice(1)); actions = script ? scriptActions(script) : []; diagnostic = actions.length ? void 0 : "Shell command could not be verified; left unchanged."; } else if (isShellScript && programScript !== void 0) actions = scriptActions(programScript); return { label, program: sanitizeForLog(program), keepAlive: /(?:^|\s|\|)keepalive(?:\s|\||$)/.test(field("properties") ?? ""), gatewayActions: actions, safeToRemove: actions.length > 0 && (field("type") === "Submitted" || Boolean(plist)), ...plistPath ? { plistPath } : {}, ...diagnostic ? { diagnostic } : {} }; } async function findForeignLaunchdJobs(env = process.env) { if (process.platform !== "darwin") return []; const result = await execLaunchctl(["list"], INSPECTION_TIMEOUT_MS); if (result.code !== 0) throw new Error(`Cannot list launchd jobs: ${formatLaunchctlResultDetail(result)}`); const labels = [...new Set(result.stdout.split(/\r?\n/).flatMap((line) => { const label = line.trim().match(/^(?:-|\d+)\s+-?\d+\s+(\S+)$/)?.[1]; return label && isCandidate(label, env) ? [label] : []; }))].toSorted(); if (labels.length > MAX_JOBS) throw new Error(`Too many OpenClaw launchd jobs to inspect safely (${labels.length}; limit ${MAX_JOBS}).`); const jobs = []; const deadline = Date.now() + 1e4; for (const label of labels) { if (Date.now() >= deadline) throw new Error("OpenClaw launchd job inspection exceeded its 10-second budget."); const job = await inspectJob(label, env); if (job) jobs.push(job); } return jobs; } function formatForeignLaunchdJobs(jobs) { return jobs.map((job) => [ `${job.label}: program=${job.program}, keepalive=${job.keepAlive}, Gateway lifecycle=${job.gatewayActions.join("|") || "not verified"}`, job.safeToRemove ? " Removable with openclaw doctor --fix." : " Report only; left unchanged.", ...job.diagnostic ? [` ${job.diagnostic}`] : [] ].join("\n")).join("\n"); } async function repairForeignLaunchdJob(job, env = process.env) { if (process.platform !== "darwin" || !isCandidate(job.label, env)) return { removed: false, detail: "Protected or unrelated launchd label; left unchanged." }; const current = await inspectJob(job.label, env); if (!current?.safeToRemove) return { removed: false, detail: "Gateway lifecycle command no longer verified; left unchanged." }; const target = `${resolveLaunchAgentGuiDomain()}/${current.label}`; if (current.plistPath) { const disabled = await execLaunchctl(["disable", target], INSPECTION_TIMEOUT_MS); if (disabled.code !== 0) return { removed: false, detail: `Could not disable ${current.label}: ${formatLaunchctlResultDetail(disabled)}` }; const refreshed = await inspectJob(current.label, env); if (!refreshed?.safeToRemove || refreshed.program !== current.program || refreshed.plistPath !== current.plistPath) return { removed: false, detail: `Disabled ${current.label}, but its definition changed before removal; inspect it manually.` }; } const removed = await execLaunchctl(["bootout", target], INSPECTION_TIMEOUT_MS); if (removed.code !== 0 && !isLaunchctlNotLoaded(removed)) return { removed: false, detail: `Could not remove ${current.label}: ${formatLaunchctlResultDetail(removed)}` }; const probe = await execLaunchctl(["print", target], INSPECTION_TIMEOUT_MS); return isLaunchctlNotLoaded(probe) ? { removed: true, detail: `Removed stray launchd job ${current.label} (${current.program}; Gateway ${current.gatewayActions.join("/")}).${current.plistPath ? " Disabled at login; plist retained." : ""}` } : { removed: false, detail: `Removal of ${current.label} could not be confirmed; inspect launchctl print ${target}.` }; } //#endregion export { formatForeignLaunchdJobs as n, repairForeignLaunchdJob as r, findForeignLaunchdJobs as t };