openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
275 lines (274 loc) • 12.7 kB
JavaScript
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 };