UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

294 lines (293 loc) 12.3 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js"; import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js"; import { s as loadWorkspaceSkillEntries } from "./workspace-_gXfieOh.js"; import { t as bumpSkillsSnapshotVersion } from "./refresh-state-DzUL33ps.js"; import { t as listAgentWorkspaceDirs } from "./workspace-dirs-9oiTy1Sm.js"; import { c as updatePairedNodeMetadata, r as listNodePairing } from "./node-pairing-BjnR-f-1.js"; //#region src/skills/runtime/remote.ts const log = createSubsystemLogger("gateway/skills-remote"); const remoteNodes = /* @__PURE__ */ new Map(); const remoteBinProbeInflight = /* @__PURE__ */ new Map(); let remoteRegistry = null; function describeNode(nodeId) { const record = remoteNodes.get(nodeId); const name = record?.displayName?.trim(); const base = name && name !== nodeId ? `${name} (${nodeId})` : nodeId; const ip = record?.remoteIp?.trim(); return ip ? `${base} @ ${ip}` : base; } function extractErrorMessage(err) { if (!err) return; if (typeof err === "string") return err; if (err instanceof Error) return err.message; if (typeof err === "object" && "message" in err && typeof err.message === "string") return err.message; if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") return String(err); if (typeof err === "symbol") return err.toString(); if (typeof err === "object") try { return JSON.stringify(err); } catch { return; } } function logRemoteBinProbeFailure(nodeId, err, context) { const message = extractErrorMessage(err); const label = describeNode(nodeId); const details = [ context?.command ? `command=${context.command}` : void 0, typeof context?.timeoutMs === "number" ? `timeoutMs=${context.timeoutMs}` : void 0, typeof context?.requiredBinCount === "number" ? `requiredBins=${context.requiredBinCount}` : void 0, `connected=${remoteNodes.get(nodeId)?.connected === true ? "yes" : "no"}` ].filter(Boolean).join(" "); if (message?.includes("node not connected") || message?.includes("node disconnected")) { log.info(`remote bin probe skipped: node unavailable (${label}; ${details})`); return; } if (message?.includes("invoke timed out") || message?.includes("timeout")) { log.warn(`remote bin probe timed out (${label}; ${details}); check node connectivity for ${label}`); return; } log.warn(`remote bin probe error (${label}; ${details}): ${message ?? "unknown"}`); } function logRemoteBinProbePreflightFailure(nodeId, err, context) { const message = extractErrorMessage(err); const label = describeNode(nodeId); const details = [ context?.command ? `command=${context.command}` : void 0, typeof context?.timeoutMs === "number" ? `timeoutMs=${context.timeoutMs}` : void 0, typeof context?.requiredBinCount === "number" ? `requiredBins=${context.requiredBinCount}` : void 0, `connected=${remoteNodes.get(nodeId)?.connected === true ? "yes" : "no"}` ].filter(Boolean).join(" "); log.info(`remote bin probe skipped: node connectivity unavailable (${label}; ${details}): ${message ?? "unknown"}`); } function isMacPlatform(platform, deviceFamily) { const platformNorm = normalizeLowercaseStringOrEmpty(platform); const familyNorm = normalizeLowercaseStringOrEmpty(deviceFamily); if (platformNorm.includes("mac")) return true; if (platformNorm.includes("darwin")) return true; if (familyNorm === "mac") return true; return false; } function supportsSystemRun(commands) { return Array.isArray(commands) && commands.includes("system.run"); } function supportsSystemWhich(commands) { return Array.isArray(commands) && commands.includes("system.which"); } function upsertNode(record) { const existing = remoteNodes.get(record.nodeId); const bins = new Set(record.bins ?? existing?.bins ?? []); remoteNodes.set(record.nodeId, { nodeId: record.nodeId, displayName: record.displayName ?? existing?.displayName, platform: record.platform ?? existing?.platform, deviceFamily: record.deviceFamily ?? existing?.deviceFamily, commands: record.commands ?? existing?.commands, remoteIp: record.remoteIp ?? existing?.remoteIp, bins, connected: record.connected ?? existing?.connected ?? false }); } function clearRemoteNodeBins(nodeId) { const existing = remoteNodes.get(nodeId); if (!existing || existing.bins.size === 0) return false; existing.bins = /* @__PURE__ */ new Set(); return true; } function setSkillsRemoteRegistry(registry) { remoteRegistry = registry; } async function primeRemoteSkillsCache() { try { const list = await listNodePairing(); let sawMac = false; for (const node of list.paired) { upsertNode({ nodeId: node.nodeId, displayName: node.displayName, platform: node.platform, deviceFamily: node.deviceFamily, commands: node.commands, remoteIp: node.remoteIp, bins: node.bins, connected: false }); if (node.bins && node.bins.length > 0 && isMacPlatform(node.platform, node.deviceFamily) && supportsSystemRun(node.commands)) sawMac = true; } if (sawMac) bumpSkillsSnapshotVersion({ reason: "remote-node" }); } catch (err) { log.warn(`failed to prime remote skills cache: ${String(err)}`); } } function recordRemoteNodeInfo(node) { upsertNode({ ...node, connected: true }); } function recordRemoteNodeBins(nodeId, bins) { upsertNode({ nodeId, bins }); } function removeRemoteNodeInfo(nodeId) { const existing = remoteNodes.get(nodeId); remoteNodes.delete(nodeId); if (existing && isMacPlatform(existing.platform, existing.deviceFamily) && supportsSystemRun(existing.commands)) bumpSkillsSnapshotVersion({ reason: "remote-node" }); } function collectRequiredBins(entries, targetPlatform) { const bins = /* @__PURE__ */ new Set(); for (const entry of entries) { const os = entry.metadata?.os ?? []; if (os.length > 0 && !os.includes(targetPlatform)) continue; const required = entry.metadata?.requires?.bins ?? []; const anyBins = entry.metadata?.requires?.anyBins ?? []; for (const bin of required) if (bin.trim()) bins.add(bin.trim()); for (const bin of anyBins) if (bin.trim()) bins.add(bin.trim()); } return [...bins]; } function buildBinProbeScript(bins) { return `for b in ${bins.map((bin) => `'${bin.replace(/'/g, `'\\''`)}'`).join(" ")}; do if command -v "$b" >/dev/null 2>&1; then echo "$b"; fi; done`; } function parseBinProbePayload(payloadJSON, payload) { if (!payloadJSON && !payload) return []; try { const parsed = payloadJSON ? JSON.parse(payloadJSON) : payload; if (Array.isArray(parsed.bins)) return normalizeStringEntries(parsed.bins); if (parsed.bins && typeof parsed.bins === "object") return Object.entries(parsed.bins).filter(([, resolvedPath]) => normalizeOptionalString(resolvedPath) !== void 0).map(([bin]) => normalizeOptionalString(bin) ?? "").filter(Boolean); if (typeof parsed.stdout === "string") return parsed.stdout.split(/\r?\n/).map((line) => normalizeOptionalString(line) ?? "").filter(Boolean); } catch { return []; } return []; } function areBinSetsEqual(a, b) { if (!a) return false; if (a.size !== b.size) return false; for (const bin of b) if (!a.has(bin)) return false; return true; } async function refreshRemoteNodeBins(params) { const existing = remoteBinProbeInflight.get(params.nodeId); if (existing) { await existing; return; } const run = refreshRemoteNodeBinsUncoalesced(params).finally(() => { if (remoteBinProbeInflight.get(params.nodeId) === run) remoteBinProbeInflight.delete(params.nodeId); }); remoteBinProbeInflight.set(params.nodeId, run); await run; } async function refreshRemoteNodeBinsUncoalesced(params) { if (!remoteRegistry) return; if (!isMacPlatform(params.platform, params.deviceFamily)) return; const canWhich = supportsSystemWhich(params.commands); const canRun = supportsSystemRun(params.commands); if (!canWhich && !canRun) return; const workspaceDirs = listAgentWorkspaceDirs(params.cfg); const requiredBins = /* @__PURE__ */ new Set(); for (const workspaceDir of workspaceDirs) { const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg }); for (const bin of collectRequiredBins(entries, "darwin")) requiredBins.add(bin); } if (requiredBins.size === 0) return; const binsList = [...requiredBins]; const timeoutMs = params.timeoutMs ?? 15e3; const command = canWhich ? "system.which" : "system.run"; const logContext = { command, timeoutMs, requiredBinCount: binsList.length }; const connectivityTimeoutMs = Math.min(timeoutMs, 2e3); if (typeof remoteRegistry.checkConnectivity === "function") { const preflightConnId = remoteRegistry.get(params.nodeId)?.connId; const connectivity = await remoteRegistry.checkConnectivity(params.nodeId, connectivityTimeoutMs); if (!connectivity.ok) { const latestSession = remoteRegistry.get(params.nodeId); if (preflightConnId && latestSession && latestSession.connId !== preflightConnId) { await refreshRemoteNodeBinsUncoalesced({ nodeId: latestSession.nodeId, platform: latestSession.platform, deviceFamily: latestSession.deviceFamily, commands: latestSession.commands, cfg: params.cfg, timeoutMs: params.timeoutMs }); return; } const cleared = clearRemoteNodeBins(params.nodeId); logRemoteBinProbePreflightFailure(params.nodeId, connectivity.error.message, { command: "websocket.ping", timeoutMs: connectivityTimeoutMs, requiredBinCount: binsList.length }); if (cleared) bumpSkillsSnapshotVersion({ reason: "remote-node" }); return; } } try { const res = await remoteRegistry.invoke(canWhich ? { nodeId: params.nodeId, command, params: { bins: binsList }, timeoutMs } : { nodeId: params.nodeId, command, params: { command: [ "/bin/sh", "-lc", buildBinProbeScript(binsList) ] }, timeoutMs }); if (!res.ok) { const cleared = clearRemoteNodeBins(params.nodeId); logRemoteBinProbeFailure(params.nodeId, res.error?.message ?? "unknown", logContext); if (cleared) bumpSkillsSnapshotVersion({ reason: "remote-node" }); return; } const bins = parseBinProbePayload(res.payloadJSON, res.payload); const existingBins = remoteNodes.get(params.nodeId)?.bins; const hasChanged = !areBinSetsEqual(existingBins, new Set(bins)); recordRemoteNodeBins(params.nodeId, bins); if (!hasChanged) return; await updatePairedNodeMetadata(params.nodeId, { bins }); bumpSkillsSnapshotVersion({ reason: "remote-node" }); } catch (err) { const cleared = clearRemoteNodeBins(params.nodeId); logRemoteBinProbeFailure(params.nodeId, err, logContext); if (cleared) bumpSkillsSnapshotVersion({ reason: "remote-node" }); } } function getRemoteSkillEligibility(options) { const macNodes = [...remoteNodes.values()].filter((node) => node.connected && isMacPlatform(node.platform, node.deviceFamily) && supportsSystemRun(node.commands)); if (macNodes.length === 0) return; const bins = /* @__PURE__ */ new Set(); for (const node of macNodes) for (const bin of node.bins) bins.add(bin); const labels = macNodes.map((node) => node.displayName ?? node.nodeId).filter(Boolean); const note = options?.advertiseExecNode === false ? void 0 : labels.length > 0 ? `Remote macOS node available (${labels.join(", ")}). Run macOS-only skills via exec host=node on that node.` : "Remote macOS node available. Run macOS-only skills via exec host=node on that node."; return { platforms: ["darwin"], hasBin: (bin) => bins.has(bin), hasAnyBin: (required) => required.some((bin) => bins.has(bin)), ...note ? { note } : {} }; } async function refreshRemoteBinsForConnectedNodes(cfg) { if (!remoteRegistry) return; const connected = remoteRegistry.listConnected(); for (const node of connected) await refreshRemoteNodeBins({ nodeId: node.nodeId, platform: node.platform, deviceFamily: node.deviceFamily, commands: node.commands, cfg }); } //#endregion export { refreshRemoteBinsForConnectedNodes as a, setSkillsRemoteRegistry as c, recordRemoteNodeInfo as i, primeRemoteSkillsCache as n, refreshRemoteNodeBins as o, recordRemoteNodeBins as r, removeRemoteNodeInfo as s, getRemoteSkillEligibility as t };