UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

275 lines (274 loc) • 12.7 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import "./session-key-DY3MOX4N.mjs"; import { n as normalizeAccountId } from "./account-id-ifCOO7_9.mjs"; import { t as AgentSelectionRequiredError } from "./agent-scope-config-BEk6-ThS.mjs"; import { t as normalizeChatType } from "./chat-type-qwvaeaJu.mjs"; import { t as normalizeAnyChannelId } from "./registry-normalize-DKVgg-js.mjs"; import { n as normalizeRouteBindingId } from "./binding-scope-BP6KauFG.mjs"; import { r as getGlobalPluginRegistry } from "./hook-runner-global-Cn3xHNvt.mjs"; import { n as getLoadedChannelPlugin } from "./registry-Bcrk1FVy.mjs"; import "./plugins-BXkKReMp.mjs"; import { t as assertConversationAuthority } from "./conversation-authority-1BN9RxJu.mjs"; import { r as PlatformMessageNotDispatchedError } from "./deliver-types-D5Rd5TTU.mjs"; import { u as withConversationAuthority } from "./conversation-registry-DcdOMU8T.mjs"; import { i as listRouteBindings } from "./bindings-Cw_QSeBA.mjs"; import { c as peerKindMatches, o as resolveAgentRoute } from "./resolve-route-DeZAxBYa.mjs"; import { n as inspectSessionBindingByConversation, r as inspectSessionBindingsByConversations } from "./session-binding-service-wvWqRl2A.mjs"; import { n as inspectRuntimeConversationBindingRoute, r as resolveConfiguredBindingRoute } from "./binding-routing-DdaOb26t.mjs"; //#region src/gateway/conversation-errors.ts /** Terminal caller/input failure for Gateway-owned conversation operations. */ var ConversationInputError = class extends Error { constructor(message) { super(message); this.name = "ConversationInputError"; } }; /** Durable operation id already belongs to a different request identity. */ var ConversationOperationConflictError = class extends ConversationInputError { constructor(message) { super(message); this.name = "ConversationOperationConflictError"; } }; //#endregion //#region src/gateway/conversation-route-ownership.ts function hasActivePluginClaimOwner(pluginId) { return getGlobalPluginRegistry()?.typedHooks.some((hook) => hook.pluginId === pluginId && hook.hookName === "inbound_claim") === true; } function pluginRouteOwnerInput(config, conversation) { return { cfg: config, accountId: normalizeAccountId(conversation.accountId), conversation: { kind: conversation.kind, peerId: conversation.peerId, target: conversation.target, ...conversation.threadId ? { threadId: conversation.threadId } : {}, ...conversation.nativeChannelId ? { nativeChannelId: conversation.nativeChannelId } : {}, ...conversation.routeContext ? { context: conversation.routeContext } : {} } }; } function resolvePluginRouteOwner(config, conversation, preparedResolver) { const channelId = normalizeAnyChannelId(conversation.channel); const resolver = preparedResolver ?? (channelId ? getLoadedChannelPlugin(channelId)?.messaging?.resolveConversationRouteOwner : void 0); if (!resolver) return; try { const owner = resolver(pluginRouteOwnerInput(config, conversation)); if (owner === void 0) return; if (owner === null) return { kind: "available" }; if (owner.kind === "unavailable") return owner; if (owner.kind === "plugin") return hasActivePluginClaimOwner(owner.pluginId) ? { kind: "available" } : { kind: "available", agentId: normalizeAgentId(owner.fallbackAgentId) }; return { kind: "available", agentId: normalizeAgentId(owner.agentId) }; } catch (error) { if (error instanceof AgentSelectionRequiredError) return { kind: "available" }; throw error; } } function resolveConfiguredRouteOwner(config, conversation, context) { try { return resolveAgentRoute({ cfg: config, channel: conversation.channel, accountId: conversation.accountId, peer: { kind: conversation.kind, id: conversation.peerId }, ...context?.parentPeerId && conversation.kind !== "direct" ? { parentPeer: { kind: conversation.kind, id: context.parentPeerId } } : {}, ...context?.guildId ? { guildId: context.guildId } : {}, ...context?.teamId ? { teamId: context.teamId } : {}, ...context?.memberRoleIds ? { memberRoleIds: context.memberRoleIds } : {} }); } catch (error) { if (error instanceof AgentSelectionRequiredError) return; throw error; } } function prepareGenericRoute(params) { const conversation = { channel: params.conversation.channel, accountId: normalizeAccountId(params.conversation.accountId), conversationId: params.conversation.peerId, ...params.context?.parentPeerId ? { parentConversationId: params.context.parentPeerId } : {} }; return { conversation, route: resolveConfiguredBindingRoute({ cfg: params.config, route: params.route, conversation }).route }; } function resolveInspectedGenericRouteOwner(route, inspection) { const runtime = inspectRuntimeConversationBindingRoute({ route, inspection }); if (runtime.bindingOwnerAvailable === false) return { kind: "unavailable" }; if (runtime.pluginId && hasActivePluginClaimOwner(runtime.pluginId)) return { kind: "available" }; return { kind: "available", agentId: normalizeAgentId(runtime.route.agentId) }; } function bindingPeerCouldMatchConversation(binding, conversation) { const peer = binding.match.peer; if (!peer) return true; const kind = normalizeChatType(peer.kind); const id = normalizeRouteBindingId(peer.id); if (!kind || !id) return false; return peerKindMatches(kind, conversation.kind) && (id === "*" || id === conversation.peerId); } function hasUnrecordedContextualBinding(params) { const channel = normalizeLowercaseStringOrEmpty(params.conversation.channel); const accountId = normalizeAccountId(params.conversation.accountId); const hasThreadContext = Boolean(params.conversation.parentConversationRef || params.conversation.threadId); const hasGuildContext = params.conversation.kind === "channel"; return listRouteBindings(params.config).some((binding) => { const pattern = binding.match.accountId?.trim() ?? ""; return Boolean(hasGuildContext && normalizeRouteBindingId(binding.match.guildId) || normalizeRouteBindingId(binding.match.teamId) || hasGuildContext && binding.match.roles?.length || hasThreadContext && binding.match.peer?.kind !== "direct" && normalizeRouteBindingId(binding.match.peer?.id)) && normalizeAgentId(binding.agentId) !== params.resolvedAgentId && normalizeLowercaseStringOrEmpty(binding.match.channel) === channel && (pattern === "*" || normalizeAccountId(pattern) === accountId) && bindingPeerCouldMatchConversation(binding, params.conversation); }); } function finishRouteEligibility(params, owner) { if (owner.kind === "unavailable") return "unavailable"; if (owner.agentId !== normalizeAgentId(params.agentId)) return "denied"; return !params.conversation.routeContextObserved && !params.conversation.routeContext && hasUnrecordedContextualBinding({ config: params.config, conversation: params.conversation, resolvedAgentId: owner.agentId }) ? "denied" : "eligible"; } function prepareRouteEligibility(params, preparedResolver) { const owner = resolvePluginRouteOwner(params.config, params.conversation, preparedResolver); if (owner) return { kind: "resolved", eligibility: finishRouteEligibility(params, owner) }; const route = resolveConfiguredRouteOwner(params.config, params.conversation, params.conversation.routeContext); if (!route) return { kind: "resolved", eligibility: "denied" }; return { kind: "binding", ...prepareGenericRoute({ ...params, route, context: params.conversation.routeContext }) }; } /** Replays current configured and plugin-owned routing for a persisted conversation address. */ function resolveConversationRouteEligibilityForAgent(params) { const prepared = prepareRouteEligibility(params); return prepared.kind === "resolved" ? prepared.eligibility : finishRouteEligibility(params, resolveInspectedGenericRouteOwner(prepared.route, inspectSessionBindingByConversation(prepared.conversation))); } /** Each grant obtains one current native selection; plugin resolvers retain precedence. */ function resolveConversationRouteEligibilitiesForAgent(params) { const inputs = params.conversations.map((conversation) => ({ config: params.config, agentId: params.agentId, conversation })); const groups = /* @__PURE__ */ new Map(); for (const [index, input] of inputs.entries()) { const channel = normalizeAnyChannelId(input.conversation.channel); const prepare = channel ? getLoadedChannelPlugin(channel)?.messaging?.prepareConversationRouteOwners : void 0; if (prepare) { const group = groups.get(prepare) ?? []; group.push(index); groups.set(prepare, group); } } const resolvers = /* @__PURE__ */ new Map(); for (const [prepare, indexes] of groups) { let preparing = true; let selected; try { selected = prepare(indexes.map((index) => pluginRouteOwnerInput(params.config, inputs[index].conversation)), (refs) => { if (!preparing) throw new Error("Conversation route preparation is no longer active"); return inspectSessionBindingsByConversations(refs); }); } finally { preparing = false; } if (selected.length !== indexes.length) throw new Error("Plugin route owner returned an incomplete selection"); indexes.forEach((index, position) => { const resolver = selected[position]; if (!resolver) throw new Error("Plugin route owner returned an incomplete selection"); resolvers.set(index, resolver); }); } const prepared = inputs.map((input, index) => prepareRouteEligibility(input, resolvers.get(index))); const inspections = inspectSessionBindingsByConversations(prepared.flatMap((entry) => entry.kind === "binding" ? [entry.conversation] : [])); let index = 0; return prepared.map((entry, position) => entry.kind === "resolved" ? entry.eligibility : finishRouteEligibility(inputs[position], resolveInspectedGenericRouteOwner(entry.route, inspections[index++]))); } /** Enforces current route ownership at a Gateway request boundary. */ function assertConversationRouteEligibleForAgent(params) { const eligibility = resolveConversationRouteEligibilityForAgent(params); if (eligibility === "eligible") return; if (eligibility === "denied") throw new ConversationInputError(`Conversation is not available to this agent: ${params.conversation.conversationRef}`); throw new Error(`Conversation ownership is temporarily unavailable: ${params.conversation.conversationRef}`); } function assertConversationDeliveryRouteAuthorized(params) { const conversation = params.conversation; try { assertConversationAuthority(conversation, params); } catch (cause) { throw new PlatformMessageNotDispatchedError(`Conversation is no longer available to this agent: ${params.conversationRef}`, { cause, retryable: false }); } const eligibility = resolveConversationRouteEligibilityForAgent({ config: params.config, agentId: params.agentId, conversation }); if (eligibility === "eligible") return; throw new PlatformMessageNotDispatchedError(eligibility === "unavailable" ? `Conversation ownership is temporarily unavailable: ${params.conversationRef}` : `Conversation is no longer available to this agent: ${params.conversationRef}`, { cause: void 0, retryable: eligibility === "unavailable" }); } function withAuthorizedConversationDelivery(params, initiate) { return withConversationAuthority(params.scope, { conversationRef: params.conversationRef }, ({ conversation }) => { assertConversationDeliveryRouteAuthorized({ ...params, conversation, config: params.readCurrentConfig?.() ?? params.config }); return initiate; }); } function withAuthorizedQueuedConversationDelivery(params, capturedScope, initiate) { return withConversationAuthority(capturedScope, { operationId: params.operationId }, ({ operation, conversation }) => { if (!operation) throw new PlatformMessageNotDispatchedError(`Conversation delivery operation no longer exists: ${params.operationId}`, { cause: void 0, retryable: false }); assertConversationDeliveryRouteAuthorized({ config: params.readCurrentConfig(), agentId: capturedScope.agentId, conversationRef: operation.conversationRef, expectedRouteFingerprint: params.routeFingerprint, conversation }); return initiate; }); } //#endregion export { withAuthorizedQueuedConversationDelivery as a, withAuthorizedConversationDelivery as i, assertConversationRouteEligibleForAgent as n, ConversationInputError as o, resolveConversationRouteEligibilitiesForAgent as r, ConversationOperationConflictError as s, assertConversationDeliveryRouteAuthorized as t };