openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
475 lines (474 loc) • 19.5 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { t as listAgentWorkspaceDirs } from "./workspace-dirs-CrAmmWK4.js";
import { t as bumpSkillsSnapshotVersion } from "./refresh-state-DHnXO3IV.js";
import { a as setRemoteSkillConnectionReconciler, n as recordRemoteSkillNodeInfo, r as removeRemoteNodeSkills } from "./remote-skills-Czcw8kW6.js";
import { a as loadWorkspaceSkills } from "./workspace-skill-loader-_a5Sci8B.js";
import { a as listNodePairing, h as updatePairedNodeBins } from "./device-pairing-node-DiTk_P80.js";
//#region src/skills/runtime/remote-probe-utils.ts
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 isMacPlatform(platform, deviceFamily) {
const platformNorm = normalizeLowercaseStringOrEmpty(platform);
const familyNorm = normalizeLowercaseStringOrEmpty(deviceFamily);
return platformNorm.includes("mac") || platformNorm.includes("darwin") || familyNorm === "mac";
}
function supportsSystemRun(commands) {
return Array.isArray(commands) && commands.includes("system.run");
}
function isRemoteSkillEligibilityNode(node) {
return Boolean(node?.connected && isMacPlatform(node.platform, node.deviceFamily) && supportsSystemRun(node.commands));
}
function supportsSystemWhich(commands) {
return Array.isArray(commands) && commands.includes("system.which");
}
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;
for (const bin of [...entry.metadata?.requires?.bins ?? [], ...entry.metadata?.requires?.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 || a.size !== b.size) return false;
for (const bin of b) if (!a.has(bin)) return false;
return true;
}
//#endregion
//#region src/skills/runtime/remote.ts
const log = createSubsystemLogger("gateway/skills-remote");
const remoteNodes = /* @__PURE__ */ new Map();
const remoteNodeProbeStates = /* @__PURE__ */ new Map();
const remoteBinProbeInflight = /* @__PURE__ */ new Map();
let remoteRegistry = null;
const REMOTE_BIN_PROBE_SUCCESS_TTL_MS = 18e5;
const REMOTE_BIN_PROBE_FAILURE_BASE_BACKOFF_MS = 15e3;
const REMOTE_BIN_PROBE_FAILURE_MAX_BACKOFF_MS = 3e5;
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 resolveRemoteBinProbeLogContext(nodeId, context) {
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(" ");
return {
label: describeNode(nodeId),
details
};
}
function logRemoteBinProbeFailure(nodeId, err, context, phase = "probe") {
const message = extractErrorMessage(err);
const { label, details } = resolveRemoteBinProbeLogContext(nodeId, context);
if (phase === "preflight") {
log.info(`remote bin probe skipped: node connectivity unavailable (${label}; ${details}): ${message ?? "unknown"}`);
return;
}
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 upsertNode(record, options) {
const existing = remoteNodes.get(record.nodeId);
const pairingGeneration = options?.pairingGenerationAuthoritative ? record.pairingGeneration : record.pairingGeneration ?? existing?.pairingGeneration;
const pairingGenerationChanged = Boolean(options?.pairingGenerationAuthoritative && existing && existing.pairingGeneration !== record.pairingGeneration);
const bins = new Set(record.bins ?? (pairingGenerationChanged ? [] : existing?.bins ?? []));
remoteNodes.set(record.nodeId, {
nodeId: record.nodeId,
connId: record.connId ?? existing?.connId,
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,
...pairingGeneration ? { pairingGeneration } : {},
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 buildRemoteProbeSignature(params) {
return JSON.stringify([
params.command,
normalizeLowercaseStringOrEmpty(params.platform),
normalizeLowercaseStringOrEmpty(params.deviceFamily),
[...params.commands ?? []].toSorted(),
params.bins.toSorted()
]);
}
function shouldSkipRemoteNodeProbe(params) {
return params.state?.pairingGeneration === params.pairingGeneration && params.state.signature === params.signature && params.nowMs < params.state.nextProbeAfterMs;
}
function restoreCachedRemoteNodeBins(nodeId) {
const node = remoteNodes.get(nodeId);
const state = remoteNodeProbeStates.get(nodeId);
const cachedBins = state?.bins;
if (!node || state?.pairingGeneration !== node.pairingGeneration || !cachedBins || areBinSetsEqual(node.bins, cachedBins)) return false;
node.bins = new Set(cachedBins);
return true;
}
function sameRemoteNodeOwner(left, right) {
return left.connId === right.connId && left.pairingGeneration === right.pairingGeneration;
}
function isCurrentRemoteNodeOwner(nodeId, owner) {
const current = remoteNodes.get(nodeId);
return Boolean(current && current.pairingGeneration === owner.pairingGeneration && (!owner.connId || !current.connId || current.connId === owner.connId));
}
function markRemoteNodeProbeSuccess(params) {
if (!isCurrentRemoteNodeOwner(params.nodeId, params.owner)) return false;
remoteNodeProbeStates.set(params.nodeId, {
signature: params.signature,
pairingGeneration: params.owner.pairingGeneration,
nextProbeAfterMs: params.nowMs + REMOTE_BIN_PROBE_SUCCESS_TTL_MS,
failedProbeCount: 0,
bins: new Set(params.bins)
});
return true;
}
function recordRemoteNodeProbeFailure(params, err, context, phase) {
if (!isCurrentRemoteNodeOwner(params.nodeId, params.owner)) return;
const existing = remoteNodeProbeStates.get(params.nodeId);
const failedProbeCount = existing?.signature === params.signature ? existing.failedProbeCount + 1 : 1;
const backoffMs = Math.min(REMOTE_BIN_PROBE_FAILURE_MAX_BACKOFF_MS, REMOTE_BIN_PROBE_FAILURE_BASE_BACKOFF_MS * 2 ** (failedProbeCount - 1));
remoteNodeProbeStates.set(params.nodeId, {
signature: params.signature,
pairingGeneration: params.owner.pairingGeneration,
nextProbeAfterMs: params.nowMs + backoffMs,
failedProbeCount
});
const cleared = clearRemoteNodeBins(params.nodeId);
logRemoteBinProbeFailure(params.nodeId, err, context, phase);
if (cleared) bumpSkillsSnapshotVersion({ reason: "remote-node" });
}
function remoteConnectionKey(nodeId, connId) {
return `${nodeId}\0${connId}`;
}
function listCurrentRemoteSessions() {
return remoteRegistry?.listCurrentConnectedSync() ?? [];
}
function listCurrentRemoteConnectionKeys() {
if (!remoteRegistry) return;
return new Set(listCurrentRemoteSessions().map((node) => remoteConnectionKey(node.nodeId, node.connId)));
}
function setSkillsRemoteRegistry(registry) {
remoteRegistry = registry;
setRemoteSkillConnectionReconciler(registry ? () => listCurrentRemoteConnectionKeys() : null);
if (!registry) remoteNodeProbeStates.clear();
}
async function primeRemoteSkillsCache() {
try {
const { paired } = await listNodePairing(void 0, { includePairingGeneration: true });
let sawMac = false;
for (const node of paired) {
if (!node.pairingGeneration) continue;
upsertNode({
nodeId: node.nodeId,
displayName: node.displayName,
platform: node.platform,
deviceFamily: node.deviceFamily,
commands: node.commands,
remoteIp: node.remoteIp,
bins: node.bins,
pairingGeneration: node.pairingGeneration,
connected: false
}, { pairingGenerationAuthoritative: true });
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) {
const existing = remoteNodes.get(node.nodeId);
const wasEligible = isRemoteSkillEligibilityNode(existing);
if (Boolean(existing && existing.pairingGeneration !== node.pairingGeneration) || node.connId && existing?.connId !== node.connId && !remoteNodeProbeStates.get(node.nodeId)?.bins) remoteNodeProbeStates.delete(node.nodeId);
upsertNode({
...node,
connected: true
}, { pairingGenerationAuthoritative: true });
if (wasEligible !== isRemoteSkillEligibilityNode(remoteNodes.get(node.nodeId))) bumpSkillsSnapshotVersion({ reason: "remote-node" });
recordRemoteSkillNodeInfo({
nodeId: node.nodeId,
connId: node.connId,
displayName: node.displayName,
commands: node.commands
});
}
function recordRemoteNodeBins(nodeId, bins, pairingGeneration) {
upsertNode({
nodeId,
bins,
pairingGeneration
});
}
function removeRemoteNodeInfo(nodeId) {
const existing = remoteNodes.get(nodeId);
remoteNodes.delete(nodeId);
removeRemoteNodeSkills(nodeId);
const probeState = remoteNodeProbeStates.get(nodeId);
if (probeState && !probeState.bins) remoteNodeProbeStates.delete(nodeId);
if (existing && isMacPlatform(existing.platform, existing.deviceFamily) && supportsSystemRun(existing.commands)) bumpSkillsSnapshotVersion({ reason: "remote-node" });
}
/** Remove remote projections only while they still belong to the invalidated connection. */
function removeRemoteNodeInfoForConnection(nodeId, connId) {
if (remoteNodes.get(nodeId)?.connId !== connId) return false;
removeRemoteNodeInfo(nodeId);
return true;
}
async function refreshRemoteNodeBins(params) {
for (;;) {
const session = remoteRegistry?.get(params.nodeId);
if (!session?.pairingGeneration) return;
const owner = {
connId: session.connId,
pairingGeneration: session.pairingGeneration
};
const existing = remoteBinProbeInflight.get(params.nodeId);
if (existing) {
await existing.promise;
if (sameRemoteNodeOwner(existing, owner)) return;
continue;
}
const inflight = {
...owner,
promise: Promise.resolve()
};
const run = refreshRemoteNodeBinsUncoalesced(params).finally(() => {
if (remoteBinProbeInflight.get(params.nodeId) === inflight) remoteBinProbeInflight.delete(params.nodeId);
});
inflight.promise = run;
remoteBinProbeInflight.set(params.nodeId, inflight);
await run;
return;
}
}
async function refreshRemoteNodeBinsUncoalesced(params) {
const readinessDelayMs = params.readinessDelayMs ?? 0;
if (readinessDelayMs > 0) await new Promise((resolve) => {
setTimeout(resolve, readinessDelayMs);
});
if (!remoteRegistry) return;
const liveSession = remoteRegistry.get(params.nodeId);
if (!liveSession?.pairingGeneration) return;
const probeOwner = {
connId: liveSession.connId,
pairingGeneration: liveSession.pairingGeneration
};
const platform = liveSession?.platform ?? params.platform;
const deviceFamily = liveSession?.deviceFamily ?? params.deviceFamily;
const commands = liveSession?.commands ?? params.commands;
if (!isMacPlatform(platform, deviceFamily)) return;
const canWhich = supportsSystemWhich(commands);
const canRun = supportsSystemRun(commands);
if (!canWhich && !canRun) return;
const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
const requiredBins = /* @__PURE__ */ new Set();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkills(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 probeSignature = buildRemoteProbeSignature({
command,
platform,
deviceFamily,
commands,
bins: binsList
});
const nowMs = Date.now();
if (shouldSkipRemoteNodeProbe({
state: remoteNodeProbeStates.get(params.nodeId),
pairingGeneration: probeOwner.pairingGeneration,
signature: probeSignature,
nowMs
})) {
if (restoreCachedRemoteNodeBins(params.nodeId)) bumpSkillsSnapshotVersion({ reason: "remote-node" });
return;
}
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;
let connectivity;
try {
connectivity = await remoteRegistry.checkConnectivity(params.nodeId, connectivityTimeoutMs);
} catch (err) {
recordRemoteNodeProbeFailure({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now()
}, err, {
command: "websocket.ping",
timeoutMs: connectivityTimeoutMs,
requiredBinCount: binsList.length
}, "preflight");
return;
}
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;
}
recordRemoteNodeProbeFailure({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now()
}, connectivity.error.message, {
command: "websocket.ping",
timeoutMs: connectivityTimeoutMs,
requiredBinCount: binsList.length
}, "preflight");
return;
}
}
try {
const res = await remoteRegistry.invoke(canWhich ? {
nodeId: params.nodeId,
expectedPairingGeneration: probeOwner.pairingGeneration,
command,
params: { bins: binsList },
timeoutMs
} : {
nodeId: params.nodeId,
expectedPairingGeneration: probeOwner.pairingGeneration,
command,
params: { command: [
"/bin/sh",
"-lc",
buildBinProbeScript(binsList)
] },
timeoutMs
});
if (!res.ok) {
recordRemoteNodeProbeFailure({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now()
}, res.error?.message ?? "unknown", logContext);
return;
}
const bins = parseBinProbePayload(res.payloadJSON, res.payload);
if (!isCurrentRemoteNodeOwner(params.nodeId, probeOwner)) return;
const existingBins = remoteNodes.get(params.nodeId)?.bins;
const hasChanged = !areBinSetsEqual(existingBins, new Set(bins));
if (hasChanged) {
if (!await updatePairedNodeBins(params.nodeId, bins, {
nodeId: params.nodeId,
key: probeOwner.pairingGeneration
})) return;
}
if (!markRemoteNodeProbeSuccess({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now(),
bins
})) return;
recordRemoteNodeBins(params.nodeId, bins, probeOwner.pairingGeneration);
if (hasChanged) bumpSkillsSnapshotVersion({ reason: "remote-node" });
} catch (err) {
recordRemoteNodeProbeFailure({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now()
}, err, logContext);
}
}
function getRemoteSkillEligibility(options) {
const currentConnections = listCurrentRemoteConnectionKeys();
const macNodes = [...remoteNodes.values()].filter((node) => node.connected && (!currentConnections || node.connId !== void 0 && currentConnections.has(remoteConnectionKey(node.nodeId, node.connId))) && 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 = listCurrentRemoteSessions();
for (const node of connected) try {
await refreshRemoteNodeBins({
nodeId: node.nodeId,
platform: node.platform,
deviceFamily: node.deviceFamily,
commands: node.commands,
cfg
});
} catch (err) {
log.warn(`failed to refresh remote bins for ${describeNode(node.nodeId)}: ${String(err)}`);
}
}
//#endregion
export { refreshRemoteBinsForConnectedNodes as a, removeRemoteNodeInfoForConnection as c, recordRemoteNodeInfo as i, setSkillsRemoteRegistry as l, primeRemoteSkillsCache as n, refreshRemoteNodeBins as o, recordRemoteNodeBins as r, removeRemoteNodeInfo as s, getRemoteSkillEligibility as t };