UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

196 lines (195 loc) • 9.8 kB
import { a as asOptionalRecord, l as isStringRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs"; import { r as hasCommandProcessCleanupError } from "./exec-result-LNNPagt0.mjs"; import { n as runExec } from "./exec-DkOImFhW.mjs"; import fs from "node:fs/promises"; //#region src/daemon/launchd-plist.ts /** Reads and renders macOS LaunchAgent plists for gateway service installs. */ const LAUNCH_AGENT_POLICY = { RunAtLoad: true, KeepAlive: true, ExitTimeOut: 20, ProcessType: "Interactive", ThrottleInterval: 10, Umask: 63, StandardInPath: "/dev/null" }; const LAUNCH_AGENT_ENV_WRAPPER_SHELL = "/bin/sh"; const plistEscape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;"); function quoteLaunchAgentEnvironmentValue(value) { return `'${value.replaceAll("'", "'\\''")}'`; } function parseGeneratedEnvValue(value) { const trimmed = value.trim(); if (!trimmed.startsWith("'") || !trimmed.endsWith("'")) return trimmed; return trimmed.slice(1, -1).replaceAll("'\\''", "'"); } function resolveSiblingGeneratedEnvFilePath(envFilePath, options) { const label = options?.generatedEnvironmentLabel?.trim(); if (!label) return; const markerIndex = envFilePath.replaceAll("\\", "/").lastIndexOf("/service-env/"); if (markerIndex < 0) return; const serviceEnvDirEnd = markerIndex + 13 - 1; return `${envFilePath.slice(0, serviceEnvDirEnd)}/${label}.env`; } function isExpectedGeneratedEnvWrapperPair(wrapperPath, envFilePath, options) { if (!wrapperPath || !envFilePath) return false; if (!options) return wrapperPath.endsWith("-env-wrapper.sh"); if (options.expectedEnvironmentWrapperPath && options.expectedEnvironmentFilePath && wrapperPath === options.expectedEnvironmentWrapperPath && envFilePath === options.expectedEnvironmentFilePath) return true; const label = options.generatedEnvironmentLabel?.trim(); if (!label) return false; const normalizedWrapper = wrapperPath.replaceAll("\\", "/"); const normalizedEnvFile = envFilePath.replaceAll("\\", "/"); return normalizedWrapper.includes("/service-env/") && normalizedEnvFile.includes("/service-env/") && normalizedWrapper.includes(`${label}-env-wrapper.sh`) && normalizedEnvFile.includes(`${label}.env`); } function resolveGeneratedEnvWrapperLayout(programArguments, options) { if (programArguments[0] === "/bin/sh") { const wrapperPath = programArguments[1]; const envFilePath = programArguments[2]; if (isExpectedGeneratedEnvWrapperPair(wrapperPath, envFilePath, options) && envFilePath) return { envFilePath, commandStartIndex: 3 }; } const wrapperPath = programArguments[0]; const envFilePath = programArguments[1]; if (isExpectedGeneratedEnvWrapperPair(wrapperPath, envFilePath, options) && envFilePath) return { envFilePath, commandStartIndex: 2 }; return null; } async function readLaunchAgentEnvironmentFile(envFilePath, options) { if (envFilePath === void 0) return {}; let content = ""; const candidateEnvFilePaths = options?.requireEffective ? [envFilePath] : Array.from(new Set([ envFilePath, resolveSiblingGeneratedEnvFilePath(envFilePath, options), options?.expectedEnvironmentFilePath ].filter((candidate) => Boolean(candidate)))); for (const candidate of candidateEnvFilePaths) try { content = await fs.readFile(candidate, "utf8"); break; } catch (error) { if (options?.requireEffective) throw error; } if (!content) return {}; const environment = {}; const lines = content.split("\n"); for (let index = 0; index < lines.length; index++) { const rawLine = lines[index] ?? ""; const line = options?.requireEffective ? rawLine.trimStart() : rawLine.trim(); if (!line.trim() || line.startsWith("#")) continue; const match = line.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/); if (!match) { if (options?.requireEffective) throw new Error("Unsupported LaunchAgent environment syntax"); continue; } const key = match[1]; let value = match[2]; if (!key || value === void 0) continue; let parsedValue = parseGeneratedEnvValue(value); if (options?.requireEffective) { while (quoteLaunchAgentEnvironmentValue(parsedValue) !== value.trim() && index + 1 < lines.length) { value += `\n${lines[++index]}`; parsedValue = parseGeneratedEnvValue(value); } if (quoteLaunchAgentEnvironmentValue(parsedValue) !== value.trim()) throw new Error("Unsupported LaunchAgent environment value"); } environment[key] = parsedValue; } return environment; } const renderEnvDict = (env) => { if (!env) return ""; const entries = Object.entries(env).filter(([key, value]) => typeof value === "string" && (value.trim() || key === "NODE_OPTIONS")); if (entries.length === 0) return ""; return `\n <key>EnvironmentVariables</key>\n <dict>${entries.map(([key, value]) => `\n <key>${plistEscape(key)}</key>\n <string>${plistEscape(value?.trim() ?? "")}</string>`).join("")}\n </dict>`; }; async function normalizeLaunchdPlistXml(contents, timeoutMs = 5e3) { const { stdout } = await runExec("/usr/bin/plutil", [ "-convert", "xml1", "-o", "-", "--", "-" ], { input: contents, timeoutMs: Math.max(1, Math.min(timeoutMs, 5e3)), maxBuffer: 1048576, logOutput: false }); return stdout; } async function decodeLaunchdPlistMetadata(contents, timeoutMs) { const deadline = performance.now() + Math.min(timeoutMs ?? 5e3, 5e3); const xml = await normalizeLaunchdPlistXml(contents, deadline - performance.now()); const { stdout } = await runExec("/usr/bin/plutil", [ "-convert", "json", "-o", "-", "--", "-" ], { input: xml.replace(/<(data|date)>[\s\S]*?<\/\1>/g, "<integer>0</integer>"), timeoutMs: Math.max(1, deadline - performance.now()), maxBuffer: 1048576, logOutput: false }); return asOptionalRecord(JSON.parse(stdout)); } async function readLaunchAgentProgramArgumentsFromFile(plistPath, options) { try { const contents = await fs.readFile(plistPath).catch(async (error) => { if (hasErrnoCode(error, "ENOENT")) { if (options?.requireEffective) { if (!await fs.lstat(plistPath).then(() => false, (statError) => hasErrnoCode(statError, "ENOENT"))) throw new Error("Unreadable LaunchAgent definition"); } return null; } throw error; }); if (contents === null) return null; const plist = await decodeLaunchdPlistMetadata(contents, options?.timeoutMs); const args = plist?.ProgramArguments; const workingDirectory = plist?.WorkingDirectory; const inlineEnvironment = plist?.EnvironmentVariables; if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string") || workingDirectory !== void 0 && typeof workingDirectory !== "string" || inlineEnvironment !== void 0 && !isStringRecord(inlineEnvironment)) throw new Error("Invalid LaunchAgent command fields"); const layout = resolveGeneratedEnvWrapperLayout(args, options); const fileEnvironment = await readLaunchAgentEnvironmentFile(layout?.envFilePath, options); const effectiveProgramArguments = layout ? args.slice(layout.commandStartIndex) : args; if (options?.requireEffective && !effectiveProgramArguments[0]) throw new Error("Missing LaunchAgent command"); const environment = { ...inlineEnvironment, ...fileEnvironment }; const environmentValueSources = {}; for (const key of Object.keys(environment)) environmentValueSources[key] = !Object.hasOwn(fileEnvironment, key) ? "inline" : Object.hasOwn(inlineEnvironment ?? {}, key) ? "inline-and-file" : "file"; return { programArguments: effectiveProgramArguments, ...workingDirectory ? { workingDirectory } : {}, ...Object.keys(environment).length > 0 ? { environment } : {}, ...Object.keys(environmentValueSources).length > 0 ? { environmentValueSources } : {}, sourcePath: plistPath }; } catch (error) { if (hasCommandProcessCleanupError(error)) throw error; if (options?.requireEffective) throw new Error("Effective LaunchAgent service command could not be inspected.", { cause: error }); return null; } } function buildLaunchAgentPlist({ label, comment, programArguments, workingDirectory, stdoutPath, stderrPath, environment }) { const argsXml = programArguments.map((arg) => `\n <string>${plistEscape(arg)}</string>`).join(""); const workingDirXml = workingDirectory ? `\n <key>WorkingDirectory</key>\n <string>${plistEscape(workingDirectory)}</string>` : ""; const commentXml = comment?.trim() ? `\n <key>Comment</key>\n <string>${plistEscape(comment.trim())}</string>` : ""; const envXml = renderEnvDict(environment); const policyXml = Object.entries(LAUNCH_AGENT_POLICY).map(([key, value]) => { const type = typeof value === "number" ? "integer" : "string"; return ` <key>${key}</key>\n ${typeof value === "boolean" ? `<${value}/>` : `<${type}>${plistEscape(String(value))}</${type}>`}`; }).join("\n"); return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${plistEscape(label)}</string>\n ${commentXml}\n${policyXml}\n <key>ProgramArguments</key>\n <array>${argsXml}\n </array>\n ${workingDirXml}\n <key>StandardOutPath</key>\n <string>${plistEscape(stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${plistEscape(stderrPath)}</string>${envXml}\n </dict>\n</plist>\n`; } //#endregion export { normalizeLaunchdPlistXml as a, resolveGeneratedEnvWrapperLayout as c, decodeLaunchdPlistMetadata as i, LAUNCH_AGENT_POLICY as n, quoteLaunchAgentEnvironmentValue as o, buildLaunchAgentPlist as r, readLaunchAgentProgramArgumentsFromFile as s, LAUNCH_AGENT_ENV_WRAPPER_SHELL as t };