UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

911 lines (910 loc) 38.9 kB
import { D as resolveExpiresAtMsFromDurationMs, a as addTimerTimeoutGraceMs, g as isFutureDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { M as isPrivateNodeInvokeCommand } from "./node-commands-BC8PhxqU.js"; import { i as MAX_BUFFERED_BYTES } from "./server-constants-BrVEC7RW.js"; import { i as replaceRemoteNodeSkills, n as recordRemoteSkillNodeInfo, r as removeRemoteNodeSkills } from "./remote-skills-Czcw8kW6.js"; import { m as parseComputerUseCapabilityDescriptor } from "./computer-use-contract-CisZRwLE.js"; import { f as retainFulfilledNodeCapabilities, l as resolveNodeCommandAllowlist } from "./node-command-policy-H4FpS0aA.js"; import { a as removeConnectedNodePluginTools, i as normalizeNodePluginToolDescriptors, o as replaceConnectedNodePluginTools, t as createRegisteredNodePluginToolDescriptorMap } from "./node-plugin-tool-snapshot-CkcyxcP-.js"; import { r as setActiveNodeContext } from "./active-node-context-_qYwwG99.js"; import { t as intersectNodePermissionSurface } from "./node-pairing-surface-zCblzr6w.js"; import { n as logRejectedLargePayload } from "./diagnostic-payload-B51qzY4j.js"; import { t as resolveEffectiveComputerUseDescriptor } from "./node-computer-use-descriptor-D_tS4bop.js"; import { n as serializeNodeEvent } from "./node-invoke-request-Cfnz22m2.js"; import { a as invokePublicNodeRegistry, c as registerNodeRegistryPrivateRuntime, f as isNodeWorkerHostClientId, i as invokeLifecycleNodeRegistry, o as isNodeRegistryPendingInvokeConnectionActive, r as forgetNodeRunnerInventory, s as reconcileNodeRunnerAvailability, u as settleNodeRegistryPairingGenerationChange } from "./node-registry-private-CPqGMumT.js"; import { n as NodeInvokeStreamController } from "./node-registry.invoke-stream-BOFc1_TA.js"; import { i as NODE_SKILL_NAME_RE } from "./node-skill-constraints-BWWr2p01.js"; //#region src/gateway/node-skill-descriptors.ts const log$1 = createSubsystemLogger("gateway/node-skills"); function normalizeNodeSkillDescriptors(params) { if (params.enabled === false) return []; const normalized = []; const seen = /* @__PURE__ */ new Set(); let totalBytes = 0; let droppedCount = 0; for (const skill of params.skills ?? []) { const name = skill.name.trim(); const description = skill.description.trim(); const contentBytes = Buffer.byteLength(skill.content, "utf8"); if (!NODE_SKILL_NAME_RE.test(name) || !description || description.length > 1024 || !skill.content || contentBytes > 65536 || seen.has(name) || normalized.length >= 64 || totalBytes + contentBytes > 524288) { droppedCount += 1; continue; } seen.add(name); totalBytes += contentBytes; normalized.push({ name, description, content: skill.content }); } if (droppedCount > 0) log$1.warn(`node ${params.nodeId} published ${params.skills?.length ?? 0} skill descriptors; dropped ${droppedCount} invalid or over-limit descriptors`); return normalized.toSorted((left, right) => left.name.localeCompare(right.name, "en")); } //#endregion //#region src/gateway/node-registry.ts const NODE_SESSION_POLICIES = /* @__PURE__ */ new WeakMap(); /** Reads commands withheld by current policy from the live session's declaration. */ function readNodeSessionWithheldCommands(node) { return NODE_SESSION_POLICIES.get(node)?.withheldCommands ?? []; } const SERIALIZED_EVENT_PAYLOAD = Symbol("openclaw.serializedEventPayload"); const AUTHORIZED_SYSTEM_RUN_EVENT_GRACE_MS = 3e5; const SLOW_CONSUMER_CLOSE_CODE = 1008; const FAILED_EVENT_LOG_INTERVAL_MS = 3e4; const log = createSubsystemLogger("gateway/nodes"); const failedEventLogAtByNode = /* @__PURE__ */ new WeakMap(); function pairingBindingForSession(node) { return { identity: node.pairingIdentity, ...node.pairingGeneration ? { generation: node.pairingGeneration } : {} }; } function pairingStateMatchesBinding(binding, current) { if (!current) return false; if (binding.identity !== current.identity) return false; return !binding.generation || binding.generation === current.generation; } /** Serialize an event payload once so fanout can reuse the same JSON string. */ function serializeEventPayload(payload) { if (payload === void 0) return null; const json = JSON.stringify(payload); return typeof json === "string" ? { json, [SERIALIZED_EVENT_PAYLOAD]: true } : null; } /** Narrow values created by serializeEventPayload. */ function isSerializedEventPayload(value) { return typeof value === "object" && value !== null && value[SERIALIZED_EVENT_PAYLOAD] === true && typeof value.json === "string"; } /** Registry of currently connected Gateway nodes. */ var NodeRegistry = class { constructor(options = {}) { this.options = options; this.nodesById = /* @__PURE__ */ new Map(); this.nodesByConn = /* @__PURE__ */ new Map(); this.eventTransportsByConn = /* @__PURE__ */ new Map(); this.pendingInvokes = /* @__PURE__ */ new Map(); this.invokeStreams = new NodeInvokeStreamController({ pendingInvokes: this.pendingInvokes, sendCancel: (requestId, pending) => { const node = this.nodesById.get(pending.nodeId); if (!node || node.connId !== pending.connId || !pending.onProgress && (!isNodeWorkerHostClientId(node.clientId) || node.clientMode !== "node")) return; this.sendEventToSession(node, "node.invoke.cancel", { invokeId: requestId, nodeId: pending.nodeId }); }, isConnectionActive: (pending) => { const node = this.nodesById.get(pending.nodeId); return isNodeRegistryPendingInvokeConnectionActive({ registry: this, pending, currentNode: node }); }, isCommandAllowed: (nodeId, command) => this.isCommandAllowed(nodeId, command), sendInput: (invokeId, pending, seq, payloadJSON) => { const node = this.nodesById.get(pending.nodeId); return node ? this.sendEventToSession(node, "node.invoke.input", { id: invokeId, nodeId: pending.nodeId, seq, payloadJSON }) : false; }, onFailedResult: (pending) => { if (pending.systemRunEvent) this.forgetAuthorizedSystemRunEvent({ nodeId: pending.nodeId, connId: pending.connId, ...pending.systemRunEvent }); }, disconnectPending: (pending) => { if (pending.command === "mcp.tools.call.v1") pending.resolve({ ok: false, error: { code: "MCP_SERVER_UNAVAILABLE", message: "node host disconnected during MCP tool call" } }); else pending.resolve({ ok: false, error: { code: "DISCONNECTED", message: `node disconnected (${pending.command})` } }); } }); this.authorizedSystemRunEvents = /* @__PURE__ */ new Map(); this.pairingGenerationEventChains = /* @__PURE__ */ new Map(); this.committedConfig = options.getConfig?.(); registerNodeRegistryPrivateRuntime(this, { getNode: (nodeId) => this.nodesById.get(nodeId), isCommandAllowed: (nodeId, command) => this.isCommandAllowed(nodeId, command, this.options.getConfig?.()), listCurrentConnected: () => this.listCurrentConnected(), hasCurrentPairingStateResolver: Boolean(this.options.resolveCurrentPairingState), resolvePairingLease: async (node) => { const current = this.nodesById.get(node.nodeId); if (!current || current.connId !== node.connId || current.pairingIdentity !== node.pairingIdentity || current.pairingGeneration !== node.pairingGeneration) return { status: "stale", presenceInvalidated: false }; return await this.resolvePairingLease(this.capturePairingLease(current), { invalidateStale: false }); }, pendingInvokes: this.pendingInvokes, invokeStreams: this.invokeStreams, sendEventToSession: (node, event, payload) => { const current = this.nodesById.get(node.nodeId); return current?.connId === node.connId ? this.sendEventToSession(current, event, payload) : false; }, rememberAuthorizedSystemRunEvent: (event) => this.rememberAuthorizedSystemRunEvent(event), publishActiveNodeContext: () => this.publishActiveNodeContext() }); } listConnectedSessions() { return [...this.nodesById.values()].filter((node) => node.client.invalidated !== true); } capturePairingLease(node) { return { session: node, nodeId: node.nodeId, connId: node.connId, binding: pairingBindingForSession(node) }; } currentSessionForLease(lease) { const current = this.nodesById.get(lease.nodeId); return current === lease.session && current.connId === lease.connId && current.pairingIdentity === lease.binding.identity && current.pairingGeneration === lease.binding.generation && current.client.invalidated !== true ? current : void 0; } settlePairingLease(params) { const current = this.currentSessionForLease(params.lease); if (!current) return { status: "stale", presenceInvalidated: false }; if (params.isCurrent) return { status: "current", session: current }; return { status: "stale", presenceInvalidated: params.invalidateStale ? this.invalidateSessionForPairingChange(current) : false }; } async resolvePairingLease(lease, options) { const resolveCurrentPairingState = this.options.resolveCurrentPairingState; if (!resolveCurrentPairingState) { const current = this.currentSessionForLease(lease); return current ? { status: "current", session: current } : { status: "stale", presenceInvalidated: false }; } let currentPairingState; try { currentPairingState = await resolveCurrentPairingState(lease.nodeId); } catch { return { status: "unavailable" }; } return this.settlePairingLease({ lease, isCurrent: pairingStateMatchesBinding(lease.binding, currentPairingState), invalidateStale: options.invalidateStale }); } refreshSessionPolicy(node) { const policy = expectDefined(NODE_SESSION_POLICIES.get(node), "registered node policy missing"); const cfg = this.committedConfig; const declaredCommands = node.sessionCommandsCeiling ?? node.declaredCommands; const allowlist = cfg ? resolveNodeCommandAllowlist(cfg, { ...node, caps: node.sessionCapsCeiling ?? node.declaredCaps, commands: declaredCommands, approvedCommands: declaredCommands }) : void 0; node.commands = policy.approvedCommands.filter((command) => declaredCommands.includes(command) && (!allowlist || allowlist.has(command))); if (allowlist) policy.withheldCommands = declaredCommands.filter((command) => !allowlist.has(command)); node.caps = retainFulfilledNodeCapabilities({ caps: policy.approvedCaps, admittedCommands: node.commands, withheldCommands: declaredCommands.filter((command) => !node.commands.includes(command)) }); node.computerUse = resolveEffectiveComputerUseDescriptor({ commands: node.commands, declared: node.declaredComputerUse }); Object.assign(node.client.connect, { commands: node.commands, caps: node.caps, computerUse: node.computerUse }); const normalized = normalizeNodePluginToolDescriptors({ nodeId: node.nodeId, tools: node.declaredNodePluginTools, allowedCommands: node.commands, enabled: cfg?.gateway?.nodes?.pluginTools?.enabled, registeredDescriptors: createRegisteredNodePluginToolDescriptorMap(this.options.listRegisteredNodePluginToolCommands?.()) }); node.nodePluginTools = normalized.map((entry) => entry.descriptor); replaceConnectedNodePluginTools({ nodeId: node.nodeId, displayName: node.displayName, platform: node.platform, remoteIp: node.remoteIp, tools: normalized }); node.nodeSkills = cfg?.gateway?.nodes?.allowSkills === false ? [] : policy.skills; recordRemoteSkillNodeInfo(node); replaceRemoteNodeSkills({ nodeId: node.nodeId, displayName: node.displayName, skills: node.nodeSkills }); } isCommandAllowed(nodeId, command, liveConfig) { if (!this.committedConfig || isPrivateNodeInvokeCommand(command)) return true; const node = this.nodesById.get(nodeId); return Boolean(node?.commands.includes(command) && (!liveConfig || resolveNodeCommandAllowlist(liveConfig, node).has(command))); } refreshRuntimePolicy(config = this.committedConfig) { this.committedConfig = config; const nodes = this.listConnected(); for (const node of nodes) this.refreshSessionPolicy(node); this.invokeStreams.reconcileRuntimePolicy(); return nodes; } /** Register a websocket client as the current connection for its node id. */ register(client, opts) { return this.registerSession(client, opts); } /** Register a node whose events are delivered by an HTTP polling transport. */ registerTransport(client, opts, transport) { return this.registerSession(client, opts, transport); } registerSession(client, opts, transport) { if (!opts.pairingIdentity) throw new Error("node session registration requires pairing identity"); const connect = client.connect; const nodeId = connect.device?.id ?? connect.client.id; const previousSession = this.nodesById.get(nodeId); const previousPairingGeneration = previousSession?.pairingGeneration; const caps = connect.caps ?? []; const commands = connect.commands ?? []; const declaredCaps = connect.declaredCaps ?? caps; const declaredCommands = connect.declaredCommands ?? commands; const computerUse = connect.computerUse === void 0 ? void 0 : parseComputerUseCapabilityDescriptor(connect.computerUse); const declaredComputerUseValue = connect.declaredComputerUse; const declaredComputerUse = declaredComputerUseValue === void 0 ? computerUse : parseComputerUseCapabilityDescriptor(declaredComputerUseValue); const sessionCapsCeiling = connect.sessionCapsCeiling ?? declaredCaps; const sessionCommandsCeiling = connect.sessionCommandsCeiling ?? declaredCommands; const declaredPermissions = connect.declaredPermissions ?? connect.permissions; const permissions = opts.approvedSurface ? intersectNodePermissionSurface({ approved: opts.approvedSurface.permissions, declared: declaredPermissions }) : connect.permissions; connect.permissions = permissions; const pathEnv = connect.pathEnv; const declaredNodePluginTools = []; const nodePluginTools = []; const nodeSkills = []; const session = { nodeId, connId: client.connId, pairingIdentity: opts.pairingIdentity, ...opts.pairingGeneration ? { pairingGeneration: opts.pairingGeneration } : {}, client, clientId: connect.client.id, clientMode: connect.client.mode, displayName: connect.client.displayName, platform: connect.client.platform, version: connect.client.version, coreVersion: connect.coreVersion, uiVersion: connect.uiVersion, deviceFamily: connect.client.deviceFamily, modelIdentifier: connect.client.modelIdentifier, remoteIp: opts.remoteIp, declaredCaps, sessionCapsCeiling, caps, declaredCommands, sessionCommandsCeiling, commands, ...declaredComputerUse ? { declaredComputerUse } : {}, ...computerUse ? { computerUse } : {}, declaredNodePluginTools, nodePluginTools, nodeSkills, declaredPermissions, permissions, pathEnv, connectedAtMs: Date.now() }; NODE_SESSION_POLICIES.set(session, { approvedCaps: (opts.approvedSurface?.caps ?? caps).filter((cap) => sessionCapsCeiling.includes(cap)), approvedCommands: (opts.approvedSurface?.commands ?? commands).filter((command) => sessionCommandsCeiling.includes(command)), skills: [], withheldCommands: connect.withheldCommands ?? [] }); const replacesPresence = previousSession?.lastActiveAtMs !== void 0; forgetNodeRunnerInventory(this, client.connId); this.nodesById.set(nodeId, session); this.nodesByConn.set(client.connId, nodeId); if (previousSession && previousSession.connId !== client.connId) this.unregister(previousSession.connId); if (previousPairingGeneration && session.pairingGeneration && previousPairingGeneration !== session.pairingGeneration) this.options.onPairingGenerationChanged?.({ nodeId, previousPairingGeneration, nextPairingGeneration: session.pairingGeneration, preserveSessionState: false }); if (transport) this.eventTransportsByConn.set(client.connId, transport); else this.eventTransportsByConn.delete(client.connId); this.refreshSessionPolicy(session); if (replacesPresence) this.publishActiveNodeContext(); reconcileNodeRunnerAvailability(this, nodeId); return session; } /** Unregister one connection and reject invokes tied to that connection. */ unregister(connId) { const nodeId = this.nodesByConn.get(connId); if (!nodeId) return null; this.nodesByConn.delete(connId); this.eventTransportsByConn.delete(connId); forgetNodeRunnerInventory(this, connId); const unregistersCurrentNode = this.nodesById.get(nodeId)?.connId === connId; if (unregistersCurrentNode) { const hadPresence = this.nodesById.get(nodeId)?.lastActiveAtMs !== void 0; this.nodesById.delete(nodeId); removeConnectedNodePluginTools(nodeId); removeRemoteNodeSkills(nodeId); if (hadPresence) this.publishActiveNodeContext(); } this.invokeStreams.handleDisconnect(connId); for (const [key, event] of this.authorizedSystemRunEvents) if (event.connId === connId) this.authorizedSystemRunEvents.delete(key); reconcileNodeRunnerAvailability(this, nodeId); return unregistersCurrentNode ? nodeId : null; } /** List connected node sessions. */ listConnected() { return this.listConnectedSessions(); } /** Filter connected sessions against an already-loaded pairing-state snapshot. */ listConnectedForPairingStates(currentPairingStates) { return this.listConnectedSessions().filter((node) => { const current = currentPairingStates.get(node.nodeId); return pairingStateMatchesBinding(pairingBindingForSession(node), current); }); } /** Reconcile connected sessions through the synchronous persistent-pairing owner. */ listCurrentConnectedSync() { const isPairingStateCurrent = this.options.isPairingStateCurrent; if (!isPairingStateCurrent) return this.listConnected(); const connected = []; let invalidatedPresence = false; for (const candidate of this.listConnectedSessions()) { const lease = this.capturePairingLease(candidate); let isCurrent; try { isCurrent = isPairingStateCurrent(candidate.nodeId, lease.binding); } catch { continue; } const resolution = this.settlePairingLease({ lease, isCurrent, invalidateStale: true }); if (resolution.status === "current") connected.push(resolution.session); else if (resolution.status === "stale") invalidatedPresence ||= resolution.presenceInvalidated; } if (invalidatedPresence) this.publishActiveNodeContext(); return connected; } /** Resolve persistent pairing state before projecting connected sessions. */ async listCurrentConnected() { const resolved = await Promise.all(this.listConnectedSessions().map((node) => this.resolvePairingLease(this.capturePairingLease(node), { invalidateStale: true }))); const connected = []; let invalidatedPresence = false; for (const result of resolved) if (result.status === "current") connected.push(result.session); else if (result.status === "stale") invalidatedPresence ||= result.presenceInvalidated; if (invalidatedPresence) this.publishActiveNodeContext(); return connected; } invalidateSessionForPairingChange(node, reason = "device-pairing-changed") { if (this.nodesById.get(node.nodeId) !== node || node.client.invalidated === true) return false; node.client.invalidated = true; node.client.invalidatedReason ??= reason; forgetNodeRunnerInventory(this, node.connId); removeConnectedNodePluginTools(node.nodeId); removeRemoteNodeSkills(node.nodeId); this.invokeStreams.handleDisconnect(node.connId); for (const [key, event] of this.authorizedSystemRunEvents) if (event.connId === node.connId) this.authorizedSystemRunEvents.delete(key); reconcileNodeRunnerAvailability(this, node.nodeId); this.options.onPairingInvalidated?.({ nodeId: node.nodeId, connId: node.connId }); return node.lastActiveAtMs !== void 0; } /** Immediately retires one exact transport after its persisted pairing authority changes. */ invalidateConnectionForPairingChange(connId, reason = "device-pairing-changed") { const nodeId = this.nodesByConn.get(connId); const node = nodeId ? this.nodesById.get(nodeId) : void 0; if (!node || node.connId !== connId) return false; if (this.invalidateSessionForPairingChange(node, reason)) this.publishActiveNodeContext(); return node.client.invalidated === true; } /** Return a connected node session by node id. */ get(nodeId) { return this.getRegisteredSession(nodeId); } getRegisteredSession(nodeId) { const node = this.nodesById.get(nodeId); return node?.client.invalidated === true ? void 0 : node; } /** Return only the session authenticated for the requested persistent pairing generation. */ getForPairingGeneration(nodeId, pairingGeneration) { return this.getRegisteredSessionForPairingGeneration(nodeId, pairingGeneration); } getRegisteredSessionForPairingGeneration(nodeId, pairingGeneration) { const node = this.getRegisteredSession(nodeId); return node?.pairingGeneration === pairingGeneration ? node : void 0; } /** Revalidates that one inbound node connection still owns its persisted pairing state. */ async isConnectionCurrentPairingState(connId) { const nodeId = this.nodesByConn.get(connId); const initial = nodeId ? this.nodesById.get(nodeId) : void 0; if (!nodeId || !initial || initial.connId !== connId || initial.client.invalidated === true || !this.options.resolveCurrentPairingState) return false; const resolution = await this.resolvePairingLease(this.capturePairingLease(initial), { invalidateStale: true }); if (resolution.status === "stale" && resolution.presenceInvalidated) this.publishActiveNodeContext(); return resolution.status === "current"; } /** Stores the latest resource snapshot for the exact authenticated node connection. */ updateHostStats(params) { const node = this.getRegisteredSession(params.nodeId); if (!node || node.connId !== params.connId) return null; node.hostStats = { ...params.stats, updatedAtMs: params.observedAtMs ?? Date.now() }; return node.hostStats; } /** Updates recent input activity for the exact authenticated node connection. */ updatePresenceActivity(params) { const node = this.nodesById.get(params.nodeId); if (!node || !params.connId || node.connId !== params.connId || node.permissions?.accessibility !== true) return null; const observedAtMs = params.observedAtMs ?? Date.now(); const lastActiveAtMs = Math.max(0, observedAtMs - params.idleSeconds * 1e3); if (params.saturated !== true || node.lastActiveAtMs === void 0) node.lastActiveAtMs = Math.max(node.lastActiveAtMs ?? 0, lastActiveAtMs); node.presenceUpdatedAtMs = observedAtMs; this.publishActiveNodeContext(); return node; } /** Clears recent input activity for the exact authenticated node connection. */ clearPresenceActivity(params) { const node = this.nodesById.get(params.nodeId); if (!node || !params.connId || node.connId !== params.connId) return null; if (node.lastActiveAtMs === void 0 && node.presenceUpdatedAtMs === void 0) return false; node.lastActiveAtMs = void 0; node.presenceUpdatedAtMs = void 0; this.publishActiveNodeContext(); return true; } /** Returns the connected node with the freshest reported local input. */ getActiveNode(connectedNodes = this.listConnected()) { let active; for (const node of connectedNodes) { if (node.lastActiveAtMs === void 0) continue; if (!active || node.lastActiveAtMs > (active.lastActiveAtMs ?? 0) || node.lastActiveAtMs === active.lastActiveAtMs && (node.presenceUpdatedAtMs ?? 0) > (active.presenceUpdatedAtMs ?? 0)) active = node; } return active; } publishActiveNodeContext() { const active = this.getActiveNode(this.listConnectedSessions()); const lease = active ? this.capturePairingLease(active) : void 0; setActiveNodeContext(active ? { nodeId: active.nodeId, ...active.pairingGeneration ? { pairingGeneration: active.pairingGeneration } : {} } : null, lease ? { isCurrent: () => { if (!this.currentSessionForLease(lease)) return false; return this.options.isPairingStateCurrent ? this.options.isPairingStateCurrent(lease.nodeId, lease.binding) : true; } } : void 0); } /** Probe websocket liveness with ping/pong when the socket supports it. */ async checkConnectivity(nodeId, timeoutMs = 2e3) { const node = this.getRegisteredSession(nodeId); if (!node) return { ok: false, error: { code: "NOT_CONNECTED", message: "node not connected" } }; const currentConnectionResult = (result) => this.nodesById.get(nodeId) === node && node.client.invalidated !== true ? result : { ok: false, error: { code: "NOT_CONNECTED", message: "node connection changed during connectivity probe" } }; const eventTransport = this.eventTransportsByConn.get(node.connId); if (eventTransport) return currentConnectionResult(eventTransport.checkConnectivity ? await eventTransport.checkConnectivity(timeoutMs) : { ok: true }); const socket = node.client.socket; if (!this.isNodeWebSocketOpen(node)) return { ok: false, error: { code: "NOT_CONNECTED", message: "node socket not open" } }; if (typeof socket.ping !== "function" || typeof socket.once !== "function") return { ok: true }; const timeout = Math.max(1, Math.trunc(timeoutMs)); return await new Promise((resolve) => { let settled = false; const cleanup = () => { socket.off?.("pong", onPong); socket.off?.("close", onClose); socket.off?.("error", onError); socket.removeListener?.("pong", onPong); socket.removeListener?.("close", onClose); socket.removeListener?.("error", onError); }; const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); cleanup(); resolve(currentConnectionResult(result)); }; const onPong = () => finish({ ok: true }); const onClose = () => finish({ ok: false, error: { code: "NOT_CONNECTED", message: "node socket closed during connectivity probe" } }); const onError = (err) => finish({ ok: false, error: { code: "UNAVAILABLE", message: err instanceof Error ? err.message : "node socket error during connectivity probe" } }); const timer = setTimeout(() => finish({ ok: false, error: { code: "TIMEOUT", message: "node connectivity probe timed out" } }), timeout); socket.once?.("pong", onPong); socket.once?.("close", onClose); socket.once?.("error", onError); try { socket.ping?.(void 0, false, (err) => { if (err) finish({ ok: false, error: { code: "UNAVAILABLE", message: err.message } }); }); } catch (err) { finish({ ok: false, error: { code: "UNAVAILABLE", message: err instanceof Error ? err.message : "node ping failed" } }); } }); } updateNodePluginTools(nodeId, connId, tools) { const node = this.nodesById.get(nodeId); if (!node || node.connId !== connId || node.client.invalidated === true) return null; node.declaredNodePluginTools = [...tools]; this.refreshSessionPolicy(node); return node; } updateNodeSkills(nodeId, connId, skills) { const node = this.nodesById.get(nodeId); if (!node || node.connId !== connId || node.client.invalidated === true) return null; expectDefined(NODE_SESSION_POLICIES.get(node), "registered node policy missing").skills = normalizeNodeSkillDescriptors({ nodeId, skills }); this.refreshSessionPolicy(node); return node; } updateSurface(nodeId, surface, generationTransition) { const node = this.nodesById.get(nodeId); if (!node || node.client.invalidated === true || generationTransition !== void 0 && (node.connId !== generationTransition.expectedConnId || node.pairingIdentity !== generationTransition.expectedPairingIdentity || node.pairingGeneration !== generationTransition.expectedPairingGeneration)) return null; const policy = expectDefined(NODE_SESSION_POLICIES.get(node), "registered node policy missing"); const sessionCommandsCeiling = new Set(node.sessionCommandsCeiling ?? node.declaredCommands); policy.approvedCommands = surface.commands.filter((command) => sessionCommandsCeiling.has(command)); if ("caps" in surface) { const sessionCapsCeiling = new Set(node.sessionCapsCeiling ?? node.declaredCaps); policy.approvedCaps = (surface.caps ?? []).filter((capability) => sessionCapsCeiling.has(capability)); } this.refreshSessionPolicy(node); if ("permissions" in surface) { node.permissions = surface.permissions === void 0 ? void 0 : intersectNodePermissionSurface({ approved: surface.permissions, declared: node.declaredPermissions }); node.client.connect.permissions = node.permissions; this.clearPresenceIfAccessibilityUnavailable(node); } if (generationTransition) { const previousPairingGeneration = node.pairingGeneration; node.pairingGeneration = generationTransition.nextPairingGeneration; settleNodeRegistryPairingGenerationChange({ registry: this, nodeId, connId: node.connId, nextPairingGeneration: generationTransition.nextPairingGeneration }); reconcileNodeRunnerAvailability(this, nodeId); if (previousPairingGeneration) this.options.onPairingGenerationChanged?.({ nodeId, previousPairingGeneration, nextPairingGeneration: generationTransition.nextPairingGeneration, preserveSessionState: true }); this.publishActiveNodeContext(); } return node; } clearPresenceIfAccessibilityUnavailable(node) { if (node.permissions?.accessibility === true || node.lastActiveAtMs === void 0) return; node.lastActiveAtMs = void 0; node.presenceUpdatedAtMs = void 0; this.publishActiveNodeContext(); } async invoke(params) { return await invokePublicNodeRegistry(this, params); } /** Internal cleanup retains its owner through replies without admitting new root work. */ invokeLifecycle(params) { return invokeLifecycleNodeRegistry(this, params); } /** Send one ordered input frame to a pending streaming invoke. */ sendInvokeInput(invokeId, payload) { this.invokeStreams.sendInput(invokeId, payload); } /** Synchronous effect fence for callbacks retained across awaited host work. */ isInvokeCurrent(invokeId, nodeId, connId) { return this.invokeStreams.isPending(invokeId, nodeId, connId); } handleInvokeProgress(params) { return this.invokeStreams.handleProgress(params); } /** Continues only the exact live owner of a pending node invocation. */ runPendingInvokeContinuation(params) { return this.invokeStreams.runPendingContinuation(params); } /** Authorize an inbound system.run event against a recently issued node invoke. */ authorizeSystemRunEvent(params) { if (!params.connId || !params.sessionKey) return false; const connId = params.connId; this.pruneAuthorizedSystemRunEvents(); let match; if (params.runId) { match = this.matchAuthorizedSystemRunEvent({ nodeId: params.nodeId, connId, runId: params.runId, sessionKey: params.sessionKey }); if (!match && this.allowsLegacyMacRunIdFallback({ nodeId: params.nodeId, connId })) match = this.matchSingleAuthorizedSystemRunEvent({ nodeId: params.nodeId, connId, sessionKey: params.sessionKey }); } else { if (!this.allowsLegacyMacRunIdFallback({ nodeId: params.nodeId, connId })) return false; match = this.matchSingleAuthorizedSystemRunEvent({ nodeId: params.nodeId, connId, sessionKey: params.sessionKey }); } if (!match) return false; if (params.terminal) this.authorizedSystemRunEvents.delete(match.key); return true; } rememberAuthorizedSystemRunEvent(event) { this.pruneAuthorizedSystemRunEvents(); const authorized = { ...event, expiresAtMs: this.authorizedSystemRunEventExpiresAt(event.timeoutMs) }; this.authorizedSystemRunEvents.set(this.authorizedSystemRunEventKey(authorized), authorized); } forgetAuthorizedSystemRunEvent(event) { this.authorizedSystemRunEvents.delete(this.authorizedSystemRunEventKey(event)); } authorizedSystemRunEventExpiresAt(timeoutMs) { if (typeof timeoutMs !== "number") return null; const durationMs = addTimerTimeoutGraceMs(timeoutMs, AUTHORIZED_SYSTEM_RUN_EVENT_GRACE_MS); return resolveExpiresAtMsFromDurationMs(durationMs) ?? 0; } matchAuthorizedSystemRunEvent(params) { for (const [key, event] of this.authorizedSystemRunEvents) if (event.nodeId === params.nodeId && event.connId === params.connId && event.runId === params.runId && this.authorizedSystemRunSessionMatches(event, params.sessionKey)) return { key, event }; return null; } matchSingleAuthorizedSystemRunEvent(params) { let match = null; for (const [key, event] of this.authorizedSystemRunEvents) { if (event.nodeId !== params.nodeId || event.connId !== params.connId || !this.authorizedSystemRunSessionMatches(event, params.sessionKey)) continue; if (match) return null; match = { key, event }; } return match; } authorizedSystemRunSessionMatches(event, sessionKey) { return !event.sessionKey || event.sessionKey === sessionKey; } allowsLegacyMacRunIdFallback(params) { const node = this.nodesById.get(params.nodeId); return node?.connId === params.connId && node.clientId === "openclaw-macos" && node.platform === "darwin"; } pruneAuthorizedSystemRunEvents(now = Date.now()) { for (const [key, event] of this.authorizedSystemRunEvents) if (event.expiresAtMs !== null && !isFutureDateTimestampMs(event.expiresAtMs, { nowMs: now })) this.authorizedSystemRunEvents.delete(key); } authorizedSystemRunEventKey(params) { return `${params.nodeId}\0${params.connId}\0${params.sessionKey ?? ""}\0${params.runId}`; } handleInvokeResult(params) { return this.invokeStreams.handleResult(params); } sendEvent(nodeId, event, payload) { const node = this.nodesById.get(nodeId); if (!node) return false; return this.sendEventToSession(node, event, payload); } sendEventRaw(nodeId, event, payloadJSON) { const node = this.nodesById.get(nodeId); if (!node) return false; return this.observeEventSend(node, event, this.sendEventRawInternal(node, event, payloadJSON)); } /** Sends command-free events only to the exact authenticated pairing connection. */ async sendEventForPairingIdentity(params) { const initial = this.nodesById.get(params.nodeId); if (!initial || initial.connId !== params.connId || initial.pairingIdentity !== params.pairingIdentity || initial.client.invalidated === true || !this.options.resolveCurrentPairingState) return false; const resolution = await this.resolvePairingLease(this.capturePairingLease(initial), { invalidateStale: true }); if (resolution.status !== "current") { if (resolution.status === "stale" && resolution.presenceInvalidated) this.publishActiveNodeContext(); return false; } return this.sendEventToSession(resolution.session, params.event, params.payload); } /** Sends only to a session that still owns the requested persistent pairing generation. */ async sendEventRawForPairingGeneration(nodeId, pairingGeneration, event, payloadJSON) { const send = (this.pairingGenerationEventChains.get(nodeId) ?? Promise.resolve()).then(() => this.sendEventRawForPairingGenerationNow(nodeId, pairingGeneration, event, payloadJSON)); const tail = send.then(() => void 0, () => void 0); this.pairingGenerationEventChains.set(nodeId, tail); try { return await send; } finally { if (this.pairingGenerationEventChains.get(nodeId) === tail) this.pairingGenerationEventChains.delete(nodeId); } } async sendEventRawForPairingGenerationNow(nodeId, pairingGeneration, event, payloadJSON) { let node = this.getRegisteredSessionForPairingGeneration(nodeId, pairingGeneration); if (!node) return false; if (this.options.resolveCurrentPairingState) { const resolution = await this.resolvePairingLease(this.capturePairingLease(node), { invalidateStale: true }); if (resolution.status !== "current") { if (resolution.status === "stale" && resolution.presenceInvalidated) this.publishActiveNodeContext(); return false; } node = resolution.session; } return this.observeEventSend(node, event, this.sendEventRawInternal(node, event, payloadJSON)); } sendEventInternal(node, event, payload) { if (node.client.invalidated === true) return false; const eventTransport = this.eventTransportsByConn.get(node.connId); if (eventTransport) return eventTransport.send(event, payload); if (!this.isNodeWebSocketOpen(node)) return false; if (this.rejectSlowNodeSocket(node)) return false; try { node.client.socket.send(serializeNodeEvent(event, payload)); return true; } catch { return false; } } sendEventRawInternal(node, event, payloadJSON) { if (node.client.invalidated === true) return false; if (payloadJSON !== null && payloadJSON !== void 0 && !isSerializedEventPayload(payloadJSON)) return false; const eventTransport = this.eventTransportsByConn.get(node.connId); if (eventTransport) return eventTransport.sendRaw(event, payloadJSON); if (!this.isNodeWebSocketOpen(node)) return false; if (this.rejectSlowNodeSocket(node)) return false; try { const payloadFragment = payloadJSON ? `,"payload":${payloadJSON.json}` : ""; node.client.socket.send(`{"type":"event","event":${JSON.stringify(event)}${payloadFragment}}`); return true; } catch { return false; } } sendEventToSession(node, event, payload) { return this.observeEventSend(node, event, this.sendEventInternal(node, event, payload)); } observeEventSend(node, event, sent) { if (sent || this.nodesById.get(node.nodeId) !== node || node.client.invalidated === true) return sent; const now = Date.now(); const lastLoggedAt = failedEventLogAtByNode.get(node); if (lastLoggedAt === void 0 || now - lastLoggedAt >= FAILED_EVENT_LOG_INTERVAL_MS) { failedEventLogAtByNode.set(node, now); log.warn("node event delivery failed", { nodeId: node.nodeId, event }); } return sent; } isNodeWebSocketOpen(node) { return node.client.socket.readyState === 1; } rejectSlowNodeSocket(node) { if (!(node.client.socket.bufferedAmount > 52428800)) return false; logRejectedLargePayload({ surface: "gateway.ws.outbound_buffer", bytes: node.client.socket.bufferedAmount, limitBytes: MAX_BUFFERED_BYTES, reason: "ws_send_buffer_close" }); try { node.client.socket.close(SLOW_CONSUMER_CLOSE_CODE, "slow consumer"); } catch {} node.client.socket.terminate(); return true; } }; //#endregion export { readNodeSessionWithheldCommands as n, serializeEventPayload as r, NodeRegistry as t };