UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

478 lines (477 loc) • 24.4 kB
import { i as extractErrorCode } from "./error-coercion-DKQY4wyF.mjs"; import { i as isPidDefinitelyDead } from "./pid-alive-Cnn-Mjdg.mjs"; import { k as escapeRegExp } from "./redact-TLu2QTmL.mjs"; import { t as createSubsystemLogger } from "./subsystem-M2sQt_up.mjs"; import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-Dgz2IR4T.mjs"; import { r as resolveRuntimeScriptPosition, t as isBunRuntime } from "./runtime-binary-BAwmhhZ0.mjs"; import { a as referencesRetainedArtifact, i as readProcessWorkingDirectories, n as classifyOpenClawEntrypointPath, o as getRootOptionAwareCommandPath, r as readProcessPackageIdentity, s as parseWindowsNativeCommandLine, t as classifyOpenClawArgv } from "./gateway-process-argv-lpAC-ZVb.mjs"; import { createRequire } from "node:module"; import { spawnSync } from "node:child_process"; import fsSync, { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import os from "node:os"; import { fileURLToPath, pathToFileURL } from "node:url"; //#region src/infra/container-environment.ts let containerEnvironmentCache; function isContainerEnvironment() { return containerEnvironmentCache ??= detectContainerEnvironment(); } function detectContainerEnvironment() { if (process.env.FLY_MACHINE_ID?.trim() && process.env.FLY_APP_NAME?.trim()) return true; for (const sentinelPath of [ "/.dockerenv", "/run/.containerenv", "/var/run/.containerenv" ]) try { fsSync.accessSync(sentinelPath, fsSync.constants.F_OK); return true; } catch {} try { const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf8"); if (/\/docker\/|cri-containerd-[0-9a-f]|containerd\/[0-9a-f]{64}|\/kubepods[/.]|\blxc\b/.test(cgroup)) return true; } catch {} return false; } //#endregion //#region src/shared/rosetta-translation.ts let translated; /** * Whether this x86_64 Darwin process is running under Rosetta. * * Rosetta reads guest stack above rsp while translating some syscalls (macOS 27 reads * [rsp+0x18, rsp+0x20) for write, getattrlist, proc_info and sysctl). Koffi enters foreign * calls at the top of its private stack, so thin libc syscall wrappers called through it fault * when the next page is unmapped. Native arm64 and Intel processes never read that memory. */ function isRosettaTranslatedProcess() { if (process.platform !== "darwin" || process.arch !== "x64") return false; translated ??= os.cpus()[0]?.model.includes("Apple") === true; return translated; } //#endregion //#region src/process/supervisor/darwin-process-command.ts let native$1; let reportedForeignArguments = false; function loadNative$1() { const koffi = createRequire(import.meta.url)("koffi"); const sysctl = koffi.load("/usr/lib/libSystem.B.dylib").func("int sysctl(const int *name, unsigned int namelen, void *oldp, size_t *oldlenp, const void *newp, size_t newlen)"); const size = Buffer.alloc(8); const argMax = Buffer.alloc(4); size.writeBigUInt64LE(4n); if (sysctl(new Int32Array([1, 8]), 2, argMax, size, null, 0) !== 0 || argMax.readInt32LE() <= 4) throw new Error("Darwin process argument limit is unavailable"); const buffer = Buffer.alloc(argMax.readInt32LE()); return { readArguments(pid) { size.writeBigUInt64LE(BigInt(buffer.length)); if (sysctl(new Int32Array([ 1, 49, pid ]), 3, buffer, size, null, 0) !== 0) return { errno: koffi.errno() }; const length = Number(size.readBigUInt64LE()); if (length < 4 || length > buffer.length) throw new Error(`Incomplete Darwin process arguments for PID ${pid}`); return { bytes: buffer.subarray(0, length) }; } }; } function parseArguments(bytes, pid) { const argc = bytes.readInt32LE(); let offset = bytes.indexOf(0, 4); if (argc <= 0 || argc > bytes.length || offset <= 4) throw new Error(`Invalid Darwin process arguments for PID ${pid}`); while (offset < bytes.length && bytes[offset] === 0) offset++; const argv = []; for (let index = 0; index < argc; index++) { const end = bytes.indexOf(0, offset); if (end < 0) throw new Error(`Truncated Darwin process arguments for PID ${pid}`); argv.push(bytes.toString("utf8", offset, end)); offset = end + 1; } const marker = bytes.toString("utf8", offset).split("\0").find((entry) => entry.startsWith("OPENCLAW_SERVICE_MARKER=")); return { argv, ...marker ? { serviceMarker: marker.slice(24) } : {} }; } /** Exact argv, or observed foreign ownership when Darwin denies argument inspection. */ function readDarwinProcessCommand(pid, uid) { if (isRosettaTranslatedProcess()) throw new Error("Cannot inspect Darwin process arguments under Rosetta; run OpenClaw with native arm64 Node.js."); native$1 ??= loadNative$1(); const result = native$1.readArguments(pid); if ("bytes" in result) return parseArguments(result.bytes, pid); if (isPidDefinitelyDead(pid)) return; const currentUid = process.getuid?.(); if (uid !== void 0 && currentUid !== void 0 && uid !== currentUid) { if (!reportedForeignArguments) { reportedForeignArguments = true; createSubsystemLogger("process/census").debug("Unreadable Darwin arguments for another UID do not establish capture custody.", { pid, uid, errno: result.errno }); } return { argvUnavailable: true, uid }; } throw new Error(`Could not classify PID ${pid}: cannot inspect Darwin arguments (errno ${result.errno}).`); } //#endregion //#region src/process/supervisor/service-child-group-ownership.ts function readLinuxProcessUid(pid) { try { const lines = readFileSync(`/proc/${pid}/status`, "utf8").split("\n").filter((line) => line.startsWith("Uid:")); const ids = (lines.length === 1 ? /^Uid:[ \t]+(\d+)[ \t]+(\d+)[ \t]+(\d+)[ \t]+(\d+)[ \t]*$/.exec(lines[0]) : null)?.slice(1).map(Number); const inspectorUid = process.getuid?.(); return ids?.every((uid) => Number.isSafeInteger(uid) && uid <= 4294967295) ? ids.find((uid) => uid === inspectorUid) ?? ids[0] : void 0; } catch { return; } } /** The caller supplies native command inspection; the standalone group worker stays dependency-free. */ function* readProcessGroupMembers(timeoutMs, commandInspection) { const includeCommand = commandInspection !== void 0; if (process.platform === "linux") { const deadline = Date.now() + timeoutMs; for (const name of readdirSync("/proc")) { if (Date.now() >= deadline) throw new Error("Process group census exceeded its deadline"); if (!/^\d+$/.test(name)) continue; const pid = Number(name); let stat; let argv; let uid; let opaqueForeignOwner = false; try { stat = readFileSync(`/proc/${name}/stat`, "utf8"); if (includeCommand) { uid = readLinuxProcessUid(pid); try { argv = readFileSync(`/proc/${name}/cmdline`, "utf8").split("\0").filter(Boolean); } catch (error) { const inspectorUid = process.getuid?.(); opaqueForeignOwner = uid !== void 0 && inspectorUid !== void 0 && uid !== inspectorUid && ["EACCES", "EPERM"].includes(extractErrorCode(error) ?? ""); if (!opaqueForeignOwner) throw error; } } } catch (error) { if (pid !== process.pid && ["ENOENT", "ESRCH"].includes(extractErrorCode(error) ?? "")) continue; throw new Error(`Could not classify PID ${pid}: process command inspection failed (${extractErrorCode(error) ?? "unavailable"}).`, { cause: error }); } const match = /^(\d+) \([\s\S]*\) (\S) (\d+) (\d+)(?:\s|$)/.exec(stat); if (!match || Number(match[1]) !== pid || Date.now() >= deadline) throw new Error("Process group census is unavailable"); if (argv?.length === 0) { const flags = Number(stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[6]); if (!(Number.isInteger(flags) && (flags & 2097152) !== 0) && !isPidDefinitelyDead(pid)) { const inspectorUid = process.getuid?.(); if (uid !== void 0 && inspectorUid !== void 0 && uid !== inspectorUid) { opaqueForeignOwner = true; argv = void 0; } else throw new Error(`Could not classify PID ${pid}: live userspace process has no readable arguments.`); } } yield { pid, pgid: Number(match[4]), state: match[2], ...argv ? { command: { ppid: Number(match[3]), argv, ...uid === void 0 ? {} : { uid } } } : opaqueForeignOwner && uid !== void 0 ? { command: { ppid: Number(match[3]), argvUnavailable: true, uid } } : {} }; } if (Date.now() >= deadline) throw new Error("Process group census exceeded its deadline"); return; } if (includeCommand && process.platform !== "darwin") throw new Error(`Exact process command census is unavailable on ${process.platform}.`); const deadline = Date.now() + timeoutMs; const census = spawnSync("/bin/ps", [ "-A", "-o", includeCommand ? "pid=,pgid=,stat=,ppid=,uid=" : "pid=,pgid=,stat=" ], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 4194304 }); if (census.error || census.status !== 0) throw new Error("Process group census is unavailable"); for (const line of census.stdout.split("\n")) { if (!line.trim()) continue; const match = includeCommand ? /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(-?\d+)\s*$/.exec(line) : /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line); if (!match) throw new Error("Process group census is unavailable"); const pid = Number(match[1]); if (pid !== census.pid) { if (includeCommand && Date.now() >= deadline) throw new Error("Process group census exceeded its deadline"); const command = includeCommand ? match[3].startsWith("Z") || pid === 0 ? { argv: [] } : commandInspection?.readDarwinCommand(pid, Number(match[5]) >>> 0) : void 0; if (includeCommand && !command) continue; yield { pid, pgid: Number(match[2]), state: match[3], ...command ? { command: { ppid: Number(match[4]), ...command, uid: Number(match[5]) >>> 0 } } : {} }; } } if (includeCommand && Date.now() >= deadline) throw new Error("Process group census exceeded its deadline"); } //#endregion //#region src/infra/windows-process-census.ts let native; function loadNative() { if (process.arch !== "x64" && process.arch !== "arm64") throw new Error("Windows process census requires a 64-bit runtime."); const koffi = createRequire(import.meta.url)("koffi"); const kernel = koffi.load("kernel32.dll"); const security = koffi.load("advapi32.dll"); const nt = koffi.load("ntdll.dll"); const enumerate = kernel.func("int32_t __stdcall K32EnumProcesses(void *, uint32_t, void *)"); const open = kernel.func("void * __stdcall OpenProcess(uint32_t, int32_t, uint32_t)"); const close = kernel.func("int32_t __stdcall CloseHandle(void *)"); const error = kernel.func("uint32_t __stdcall GetLastError()"); const exitCode = kernel.func("int32_t __stdcall GetExitCodeProcess(void *, void *)"); const times = kernel.func("int32_t __stdcall GetProcessTimes(void *, void *, void *, void *, void *)"); const read = kernel.func("int32_t __stdcall ReadProcessMemory(void *, uintptr_t, void *, size_t, void *)"); const query = nt.func("int32_t __stdcall NtQueryInformationProcess(void *, uint32_t, void *, uint32_t, void *)"); const equalSid = security.func("int32_t __stdcall EqualSid(void *, void *)"); const sessions = koffi.load("wtsapi32.dll"); const owners = sessions.func("int32_t __stdcall WTSEnumerateProcessesW(void *, uint32_t, uint32_t, void *, void *)"); const free = sessions.func("void __stdcall WTSFreeMemory(void *)"); const memory = (handle, address, length) => { const bytes = Buffer.alloc(length); const returned = Buffer.alloc(8); if (!address || !read(handle, address, bytes, length, returned) || returned.readBigUInt64LE() !== BigInt(length)) throw new Error("Process memory is unreadable."); return bytes; }; const parameters = (handle, observation) => { const basic = Buffer.alloc(48); const wow64 = Buffer.alloc(8); const returned = Buffer.alloc(4); if (query(handle, 0, basic, basic.length, returned) < 0 || returned.readUInt32LE() !== basic.length || basic.readBigUInt64LE(32) !== BigInt(observation.pid) || query(handle, 26, wow64, wow64.length, returned) < 0 || returned.readUInt32LE() !== wow64.length) throw new Error("Process identity or architecture is unavailable."); const narrow = wow64.readBigUInt64LE() !== 0n; const pointer = (bytes, offset) => narrow ? BigInt(bytes.readUInt32LE(offset)) : bytes.readBigUInt64LE(offset); const peb = narrow ? wow64.readBigUInt64LE() : basic.readBigUInt64LE(8); const address = pointer(memory(handle, peb, narrow ? 20 : 40), narrow ? 16 : 32); const header = memory(handle, address, narrow ? 72 : 128); const string = (offset) => { try { const length = header.readUInt16LE(offset); if (!length || length % 2 || length > header.readUInt16LE(offset + 2)) return; const value = pointer(header, offset + (narrow ? 4 : 8)); const location = header.readUInt32LE(8) & 1 ? value : address + value; return memory(handle, location, length).toString("utf16le"); } catch { return; } }; observation.parentPid = Number(basic.readBigUInt64LE(40)); observation.commandLine = string(narrow ? 64 : 112); observation.cwd = string(narrow ? 36 : 56); }; return (deadline) => { const bytes = Buffer.alloc(262144); const returned = Buffer.alloc(4); if (!enumerate(bytes, bytes.length, returned) || returned.readUInt32LE() >= bytes.length || returned.readUInt32LE() % 4) throw new Error("Windows process enumeration is incomplete."); const pids = new Uint32Array(bytes.buffer, bytes.byteOffset, returned.readUInt32LE() / 4); const snapshot = new Map(Array.from(pids, (pid) => [pid, 0n])); const allocation = Buffer.alloc(8); try { try { if (owners(null, 0, 1, allocation, returned) && returned.readUInt32LE() <= 65536) { const records = Buffer.from(koffi.decode(allocation.readBigUInt64LE(), "uint8_t", returned.readUInt32LE() * 24)); for (let offset = 0; offset < records.length; offset += 24) snapshot.set(records.readUInt32LE(offset + 4), records.readBigUInt64LE(offset + 16)); } } catch { snapshot.forEach((_sid, pid) => snapshot.set(pid, 0n)); } const self = snapshot.get(process.pid); const observations = Array.from(snapshot, ([pid, owner]) => { if (Date.now() >= deadline) throw new Error("Windows process census exceeded its deadline."); if (pid === 0 || pid === 4) return; const observation = { pid }; if (self && owner && !equalSid(self, owner)) observation.foreignOwner = true; const handle = open(1040, 0, pid) ?? (error() === 87 ? null : open(4096, 0, pid)); if (!handle) return error() === 87 ? void 0 : observation; try { const state = Buffer.alloc(4); const known = exitCode(handle, state); if (!known || state.readUInt32LE() !== 259) return known ? void 0 : observation; const created = Buffer.alloc(8); if (times(handle, created, Buffer.alloc(8), Buffer.alloc(8), Buffer.alloc(8)) && created.readBigUInt64LE()) observation.startIdentity = String(Number(created.readBigUInt64LE() / 10000n - 11644473600000n)); try { parameters(handle, observation); } catch {} } finally { close(handle); } return observation; }); if (Date.now() >= deadline) throw new Error("Windows process census exceeded its deadline."); return observations.filter((observation) => observation !== void 0); } finally { if (allocation.readBigUInt64LE()) free(allocation.readBigUInt64LE()); } }; } /** Missing fields never establish absence for same-user or unknown-owner work. */ function readWindowsProcessCensus(timeoutMs) { const deadline = Date.now() + timeoutMs; return (native ??= loadNative())(deadline); } //#endregion //#region src/infra/openclaw-process-census.ts const workerEntrypoints = Object.values(runtimeProcessEntrypoints).flatMap((entry) => [path.posix.normalize(`src/infra/${entry.sourceWorkerName}.ts`), `dist/${entry.distWorkerPath}`]); function isStandardSystemdUserManager(command) { return command !== void 0 && "argv" in command && command.argv.length === 2 && command.argv[1] === "--user" && (command.argv[0] === "/usr/lib/systemd/systemd" || command.argv[0] === "/lib/systemd/systemd"); } function isStandardSystemdLoginProcess(command, parentCommand, inspectorUid) { if (process.platform !== "linux" || inspectorUid === void 0 || command?.uid !== inspectorUid || !("argv" in command)) return false; return isStandardSystemdUserManager(command) || command.argv.length === 1 && command.argv[0] === "(sd-pam)" && parentCommand?.uid === command.uid && isStandardSystemdUserManager(parentCommand); } function classifyProcessArtifactCustody(command, pid, cwd, parentCommand) { try { const argv = command && "argv" in command ? command.argv : []; if ([...argv, cwd ?? ""].some(referencesRetainedArtifact)) return { kind: "holder" }; if (!command) throw new Error("process command is unavailable"); if ("argvUnavailable" in command || command.argv.length === 0) return { kind: "non-holder" }; const { serviceMarker } = command; const options = { pid, cwd: cwd ?? "", serviceMarker, additionalEntrypoints: workerEntrypoints, inspectPackage: true }; const identity = classifyOpenClawArgv(argv, options); if (identity.kind === "openclaw") return { kind: "holder" }; if (identity.kind === "unclassified" && identity.cause !== "runtime-syntax") throw new Error(identity.reason); const { position, operands } = resolveRuntimeScriptPosition(argv); if (!cwd || !path.isAbsolute(cwd)) { const inspectorUid = process.getuid?.(); if (cwd === void 0 && identity.kind === "other" && typeof position !== "number" && position.kind === "not-runtime" && command.uid !== void 0 && inspectorUid !== void 0 && (command.uid !== inspectorUid || isStandardSystemdLoginProcess(command, parentCommand, inspectorUid))) return { kind: "non-holder" }; throw new Error("working directory is unavailable"); } if (typeof position !== "number" && position.kind === "not-runtime") return { kind: "non-holder" }; let scripts = {}; if (typeof position !== "number") { const pkg = readProcessPackageIdentity(cwd, true); if (pkg.name === "openclaw") return { kind: "holder" }; scripts = pkg.scripts; } for (const { value, module } of operands) { let candidate = value; if (module) { if (value.startsWith("file:")) candidate = fileURLToPath(value); else if (!path.isAbsolute(value) && !value.startsWith("./") && !value.startsWith("../")) throw new Error("runtime module package identity is unavailable"); } else if (isBunRuntime(argv[0] ?? "") && !/[\\/]/u.test(value) && !path.extname(value) && typeof scripts[value] === "string" && scripts[value].trim()) continue; const evidence = classifyOpenClawEntrypointPath(candidate, options); if (evidence.kind === "unclassified") throw new Error(evidence.reason); if (evidence.kind === "openclaw") return { kind: "holder" }; } return { kind: "non-holder" }; } catch (error) { return { kind: "unresolved", reason: error instanceof Error ? error.message : String(error) }; } } function inspectOtherOpenClawProcesses(handoff) { const result = { matchingPids: [], unverifiedPids: [] }; try { if (process.platform === "linux" && isContainerEnvironment()) throw new Error("Host process visibility cannot be established from this container. Run Doctor on the host after stopping OpenClaw containers that share its temporary directory."); const windows = process.platform === "win32"; const deadline = Date.now() + (handoff ? 15e3 : 1e3); const native = windows && handoff ? [...readWindowsProcessCensus(15e3)] : void 0; const nativeByPid = new Map(native?.map((entry) => [entry.pid, entry])); const processes = native?.map(({ pid, parentPid, commandLine }) => { const argv = commandLine ? parseWindowsNativeCommandLine(commandLine) : null; return { pid, state: "", command: argv?.length && parentPid !== void 0 ? { ppid: parentPid, argv } : void 0 }; }) ?? [...readProcessGroupMembers(handoff ? 15e3 : 1e3, { readDarwinCommand: readDarwinProcessCommand })]; const byPid = new Map(processes.map((entry) => [entry.pid, entry])); const current = byPid.get(process.pid); if (!current || !handoff && processes.some((entry) => !entry.command || process.platform === "linux" && !("argv" in entry.command))) throw new Error("OpenClaw process census is incomplete."); const directories = native ? new Map(native.map(({ pid, cwd }) => [pid, cwd])) : readProcessWorkingDirectories(processes.map(({ pid }) => pid)); const launchers = /* @__PURE__ */ new Set(); const ancestors = /* @__PURE__ */ new Set([process.pid]); let child = current; let parentPid = current.command?.ppid ?? 0; while (parentPid > 0) { const parent = byPid.get(parentPid); if (!parent?.command || ancestors.has(parentPid)) { if (handoff) break; throw new Error("OpenClaw process ancestry is incomplete."); } const parentStart = nativeByPid.get(parentPid)?.startIdentity; const childStart = nativeByPid.get(child.pid)?.startIdentity; if (windows && (!parentStart || !childStart || BigInt(parentStart) >= BigInt(childStart))) break; ancestors.add(parentPid); if ("argv" in parent.command) { const { argv, serviceMarker } = parent.command; const identity = classifyOpenClawArgv(argv, { pid: parentPid, serviceMarker, cwd: directories.get(parentPid) ?? "", additionalEntrypoints: workerEntrypoints }); if (identity.kind === "openclaw" && identity.entryIndex !== void 0 && getRootOptionAwareCommandPath(["node", ...argv.slice(identity.entryIndex)], 1)[0] === (handoff ? "update" : "doctor")) launchers.add(parentPid); } child = parent; parentPid = parent.command.ppid; } const normalize = (value) => windows ? value.replaceAll("\\", "/").replace(/\/{2,}\?\/UNC\//gi, "//").replace(/\/{2,}\?\//g, "").replace(/\/{2,}/g, "/").toLowerCase() : value; const paths = handoff?.artifactPaths.flatMap((value) => [value.replace(/[\\/]+$/, ""), pathToFileURL(value, { windows }).href]) ?? []; const literal = (value) => escapeRegExp(normalize(value)); const matches = new RegExp(`(?:^|[^\\w-])(?:${literal(handoff?.runId ?? "")}(?=$|[^\\w-])|(?:${paths.map(literal).join("|") || "(?!)"})(?=$|[/\\s"'\x60);,\\]]))`); for (const { pid, state, command } of processes) { if (handoff && Date.now() >= deadline) throw new Error("Process census deadline exceeded"); if (pid === process.pid || launchers.has(pid)) continue; if ((state?.startsWith("Z") || handoff) && isPidDefinitelyDead(pid)) continue; const argv = command && "argv" in command ? command.argv : void 0; if (handoff) { if (!windows && argv?.length === 0) continue; const observation = nativeByPid.get(pid); const cwd = directories.get(pid); if ([ ...argv ?? [], observation?.commandLine ?? "", cwd ?? "" ].some((value) => matches.test(normalize(value)))) { result.matchingPids.push(pid); continue; } const inspectorUid = process.getuid?.(); const foreign = observation?.foreignOwner || command?.uid !== void 0 && inspectorUid !== void 0 && command.uid !== inspectorUid; if ((!argv || cwd === void 0) && !foreign && !isStandardSystemdLoginProcess(command, byPid.get(command?.ppid ?? 0)?.command, inspectorUid)) { result.unverifiedPids.push(pid); if (windows) result.error = "Retry update repair as Administrator using the same Windows account."; } continue; } const custody = classifyProcessArtifactCustody(command, pid, directories.get(pid), byPid.get(command?.ppid ?? 0)?.command); if (custody.kind === "unresolved") throw new Error(`Could not classify PID ${pid}: ${custody.reason}`); if (custody.kind === "holder") result.matchingPids.push(pid); } } catch (error) { const pid = /Could not classify PID (\d+):/.exec(error instanceof Error ? error.message : ""); if (handoff && pid) result.unverifiedPids.push(Number(pid[1])); result.error = !handoff ? `Could not inspect OpenClaw processes: ${String(error)}` : isRosettaTranslatedProcess() ? "Host process census is unavailable under Rosetta; run openclaw update repair with native arm64 Node.js." : "Host process census is incomplete; verify process-inspection permissions and retry update repair."; } return handoff ? result : result.error ? { error: result.error } : { pids: result.matchingPids }; } //#endregion export { inspectOtherOpenClawProcesses };