UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

199 lines (198 loc) 7.6 kB
import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js"; import { n as NODE_DESKTOP_STREAM_COMMAND } from "./node-desktop-stream-BZM2AiRA.js"; import { l as resolveNodeCommandAllowlist, o as isNodeCommandAllowed } from "./node-command-policy-H4FpS0aA.js"; import { t as DesktopCredentialsRequiredError } from "./host-source-errors-46uOYNUn.js"; import { r as mintDesktopObserverToken } from "./observe-bridge-DyRZ6Fs5.js"; //#region src/gateway/desktop/node-source.ts function invocationError(result) { const message = result.error?.message?.trim(); return new Error(message || "node desktop stream closed before attachment"); } async function stopActiveStream(active) { if (active.stopped) return; retireActiveStream(active); await active.invocation?.catch(() => void 0); } function retireActiveStream(active) { if (active.stopped) return; active.stopped = true; clearTimeout(active.unclaimedTimer); active.ticket?.cancel(); active.controller.abort(); if (!active.reservationTransferred) active.reservation?.release(); active.stream?.destroy(); } /** Combines node command policy, ticket redemption, and desktop session ownership. */ function createNodeDesktopService(params) { const ownerEpochs = /* @__PURE__ */ new Map(); const sessions = /* @__PURE__ */ new Map(); const commandAllowed = (node) => isNodeCommandAllowed({ command: NODE_DESKTOP_STREAM_COMMAND, declaredCommands: node.commands, allowlist: resolveNodeCommandAllowlist(params.getConfig(), node) }).ok; const stopNode = async (nodeId) => { const session = sessions.get(nodeId); if (session) await params.desktopRegistry.stop(`node:${nodeId}`, session.ownerEpoch); }; const ensureSession = async (request) => { const sourceKey = `node:${request.nodeId}`; const current = sessions.get(request.nodeId); if (current?.connId === request.connId && current.pairingGeneration === request.pairingGeneration) { await params.desktopRegistry.activate({ sourceKey, ownerEpoch: current.ownerEpoch }); return current; } const ownerEpoch = (ownerEpochs.get(request.nodeId) ?? 0) + 1; ownerEpochs.set(request.nodeId, ownerEpoch); const session = { connId: request.connId, pairingGeneration: request.pairingGeneration, ownerEpoch, active: /* @__PURE__ */ new Set() }; sessions.set(request.nodeId, session); try { await params.desktopRegistry.activate({ sourceKey, ownerEpoch, teardown: async () => { if (sessions.get(request.nodeId) === session) sessions.delete(request.nodeId); await Promise.all([...session.active].map(stopActiveStream)); session.active.clear(); } }); return session; } catch (error) { if (sessions.get(request.nodeId) === session) sessions.delete(request.nodeId); throw error; } }; return { stopNode, async reconcileRuntimePolicy() { await Promise.all([...sessions].map(async ([nodeId, session]) => { const node = params.nodeRegistry.get(nodeId); if (!node || node.connId !== session.connId || node.pairingGeneration !== session.pairingGeneration || !commandAllowed(node)) await stopNode(nodeId); })); }, async observe(request) { const node = params.nodeRegistry.get(request.nodeId); if (!node?.pairingGeneration) throw new Error("node desktop is unavailable; reconnect and approve the node capability"); const pairingGeneration = node.pairingGeneration; const isAuthorized = () => params.nodeRegistry.get(request.nodeId) === node && node.pairingGeneration === pairingGeneration && commandAllowed(node); const assertAuthorized = () => { if (!isAuthorized()) throw new Error("node desktop is not enabled; explicitly allow and approve desktop.stream for this node"); }; assertAuthorized(); const sourceKey = `node:${request.nodeId}`; const session = await ensureSession({ nodeId: request.nodeId, connId: node.connId, pairingGeneration }); assertAuthorized(); const active = { controller: new AbortController(), reservation: params.desktopRegistry.reserveObserver(sourceKey, session.ownerEpoch), reservationTransferred: false, stopped: false }; if (!active.reservation) throw new Error("node desktop observer limit reached"); session.active.add(active); try { active.ticket = params.streamBroker.mint({ nodeId: request.nodeId, connId: node.connId, pairingGeneration }); active.invocation = params.nodeRegistry.invoke({ nodeId: request.nodeId, expectedConnId: node.connId, expectedPairingGeneration: pairingGeneration, command: NODE_DESKTOP_STREAM_COMMAND, params: { ticket: active.ticket.ticket, attachPath: active.ticket.attachPath }, timeoutMs: 0, onProgress: () => {}, signal: active.controller.signal, isDispatchAuthorized: () => !active.stopped && sessions.get(request.nodeId) === session && isAuthorized() }); const invocationFinished = active.invocation.then((result) => { throw invocationError(result); }); invocationFinished.catch(() => void 0); const attached = await Promise.race([active.ticket.attached, invocationFinished]); if (active.stopped || sessions.get(request.nodeId) !== session) { attached.stream.destroy(); throw new Error("node desktop session was superseded before attachment"); } active.stream = attached.stream; assertAuthorized(); let preauth; if (attached.auth === "vnc-password") { const password = attached.vncPassword ?? request.credentials?.password; if (!password) throw new DesktopCredentialsRequiredError("vnc-password", "VNC password is required to observe this node"); registerSecretValueForRedaction(password); preauth = { auth: attached.auth, credentials: { password } }; } else { const username = request.credentials?.username?.trim() ?? ""; const password = request.credentials?.password ?? ""; if (!username || !password) throw new DesktopCredentialsRequiredError("ard-account", "macOS account credentials are required to observe this node"); registerSecretValueForRedaction(password); preauth = { auth: attached.auth, credentials: { username, password } }; } const attachment = params.desktopRegistry.publishStream({ sourceKey, ownerEpoch: session.ownerEpoch, stream: attached.stream, reservation: active.reservation }); if (!attachment) throw new Error("node desktop session was superseded before publication"); active.reservationTransferred = true; const minted = mintDesktopObserverToken({ sourceKey, ownerEpoch: session.ownerEpoch, control: request.control, attachment, preauth }); active.unclaimedTimer = setTimeout(() => { if (params.desktopRegistry.hasPendingStream(sourceKey, attachment)) stopActiveStream(active).then(() => session.active.delete(active)); }, Math.max(0, minted.expiresAtMs - Date.now())); active.unclaimedTimer.unref?.(); active.invocation.finally(() => { retireActiveStream(active); session.active.delete(active); }).catch(() => void 0); return { transport: "rfb", wsPath: `/desktop/observe?token=${minted.token}`, expiresAtMs: minted.expiresAtMs, control: request.control, auth: attached.auth, preauthenticated: true }; } catch (error) { await stopActiveStream(active); session.active.delete(active); throw error; } } }; } //#endregion export { createNodeDesktopService };