openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
468 lines (467 loc) • 20 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { d as normalizeStringEntries } from "./string-normalization-YMwDjufr.mjs";
import { c as sleepWithAbort } from "./src-DqaLEvc_.mjs";
import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs";
import "./backoff-DG9ukw7j.mjs";
import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-BU4qc9bA.mjs";
import { i as listAgentIds } from "./agent-roster-B45JERT5.mjs";
import "./agent-scope-DWB4x52T.mjs";
import { t as bumpSkillsSnapshotVersion } from "./refresh-state-Cazo5oir.mjs";
import { i as removeRemoteNodeSkills, o as setRemoteSkillConnectionReconciler, r as recordRemoteSkillNodeInfo } from "./remote-skills-ml3IDwsA.mjs";
import { n as executeDevicePairingMutation, t as DevicePairingAuthorityRefusedError } from "./device-pairing-worker-TZJ_K0FD.mjs";
import { a as listNodePairing } from "./device-pairing-node-CcQMIt42.mjs";
//#region src/infra/device-pairing-node-facts.ts
/** Update remote skill bins while the probe still owns the durable node generation. */
async function updatePairedNodeBins(nodeId, bins, expectedPairingGeneration, baseDir, isProbeCurrent) {
try {
return await executeDevicePairingMutation({
type: "node.updateBins",
input: {
nodeId,
bins,
expectedPairingGeneration
}
}, {
baseDir,
assertCurrent: () => {
if (isProbeCurrent?.() === false) throw new DevicePairingAuthorityRefusedError("node bin probe ownership changed");
}
});
} catch (error) {
if (error instanceof DevicePairingAuthorityRefusedError) return false;
throw error;
}
}
/** Persist runner-host consent only while its connection still owns the durable generation. */
async function updatePairedNodeSessionHost(params) {
const { baseDir, isConnectionCurrent, ...input } = params;
try {
return await executeDevicePairingMutation({
type: "node.updateSessionHost",
input
}, {
baseDir,
assertCurrent: () => {
if (!isConnectionCurrent()) throw new DevicePairingAuthorityRefusedError("node session connection changed");
}
});
} catch (error) {
if (error instanceof DevicePairingAuthorityRefusedError) return false;
throw error;
}
}
//#endregion
//#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 normalizeStringEntries(parsed.stdout.split(/\r?\n/));
} 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 logRemoteBinProbeFailure(nodeId, err, context, phase = "probe") {
const message = extractErrorMessage(err);
const label = describeNode(nodeId);
const details = `command=${context.command} timeoutMs=${context.timeoutMs} requiredBins=${context.requiredBinCount} connected=${remoteNodes.get(nodeId)?.connected === true ? "yes" : "no"}`;
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 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 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 listCurrentRemoteConnectionKeys() {
if (!remoteRegistry) return;
return new Set(remoteRegistry.listCurrentConnectedSync().map((node) => remoteConnectionKey(node.nodeId, node.connId)));
}
function setSkillsRemoteRegistry(registry) {
remoteRegistry = registry;
setRemoteSkillConnectionReconciler(registry ? listCurrentRemoteConnectionKeys : null, registry ? () => registry.listCurrentConnected() : void 0);
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(node);
}
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) try {
await sleepWithAbort(readinessDelayMs, params.readinessSignal);
} catch (error) {
if (!params.readinessSignal?.aborted) throw error;
}
if (params.readinessSignal?.aborted) return;
if (!remoteRegistry) return;
const { loadWorkspaceSkills } = await import("./workspace-skill-loader-BU8RFqXU.mjs");
const liveSession = remoteRegistry?.get(params.nodeId);
if (params.readinessSignal?.aborted || !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 requiredBins = /* @__PURE__ */ new Set();
for (const agentId of listAgentIds(params.cfg)) {
const entries = loadWorkspaceSkills(resolveAgentWorkspaceDir(params.cfg, agentId), {
config: params.cfg,
agentId,
agentSkillFilter: "ignore"
});
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 cachedProbe = remoteNodeProbeStates.get(params.nodeId);
if (cachedProbe?.pairingGeneration === probeOwner.pairingGeneration && cachedProbe.signature === probeSignature && Date.now() < cachedProbe.nextProbeAfterMs) {
if (restoreCachedRemoteNodeBins(params.nodeId)) bumpSkillsSnapshotVersion({ reason: "remote-node" });
return;
}
const logContext = {
command,
timeoutMs,
requiredBinCount: binsList.length
};
const recordFailure = (error, context = logContext, phase) => recordRemoteNodeProbeFailure({
nodeId: params.nodeId,
owner: probeOwner,
signature: probeSignature,
nowMs: Date.now()
}, error, context, phase);
const connectivityTimeoutMs = Math.min(timeoutMs, 2e3);
if (typeof remoteRegistry.checkConnectivity === "function") {
const preflightContext = {
command: "websocket.ping",
timeoutMs: connectivityTimeoutMs,
requiredBinCount: binsList.length
};
const preflightConnId = remoteRegistry.get(params.nodeId)?.connId;
let connectivity;
try {
connectivity = await remoteRegistry.checkConnectivity(params.nodeId, connectivityTimeoutMs);
} catch (err) {
recordFailure(err, preflightContext, "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;
}
recordFailure(connectivity.error.message, preflightContext, "preflight");
return;
}
}
try {
const res = await remoteRegistry.invoke({
nodeId: params.nodeId,
expectedPairingGeneration: probeOwner.pairingGeneration,
command,
params: canWhich ? { bins: binsList } : { command: [
"/bin/sh",
"-lc",
buildBinProbeScript(binsList)
] },
timeoutMs
});
if (!res.ok) {
recordFailure(res.error?.message ?? "unknown");
return;
}
const bins = parseBinProbePayload(res.payloadJSON, res.payload);
if (!isCurrentRemoteNodeOwner(params.nodeId, probeOwner)) return;
const existingBins = remoteNodes.get(params.nodeId)?.bins;
const nextBins = new Set(bins);
const hasChanged = !areBinSetsEqual(existingBins, nextBins);
if (hasChanged) {
if (!await updatePairedNodeBins(params.nodeId, bins, {
nodeId: params.nodeId,
key: probeOwner.pairingGeneration
}, void 0, () => isCurrentRemoteNodeOwner(params.nodeId, probeOwner))) return;
}
if (!isCurrentRemoteNodeOwner(params.nodeId, probeOwner)) return;
remoteNodeProbeStates.set(params.nodeId, {
signature: probeSignature,
pairingGeneration: probeOwner.pairingGeneration,
nextProbeAfterMs: Date.now() + REMOTE_BIN_PROBE_SUCCESS_TTL_MS,
failedProbeCount: 0,
bins: nextBins
});
recordRemoteNodeBins(params.nodeId, bins, probeOwner.pairingGeneration);
if (hasChanged) bumpSkillsSnapshotVersion({ reason: "remote-node" });
} catch (err) {
recordFailure(err);
}
}
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 = await remoteRegistry.listCurrentConnected();
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, updatePairedNodeSessionHost as u };