UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,144 lines (1,143 loc) 44.5 kB
import { c as normalizeOptionalLowercaseString, g as readStringValue, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { E as normalizeSessionKeyPreservingOpaquePeerIds, M as parseThreadSessionSuffix, O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js"; import { n as normalizeAccountId, r as normalizeOptionalAccountId } from "./account-id-CETVCrTz.js"; import { C as selectApplicableRuntimeConfig, l as getRuntimeConfigSourceSnapshot, o as getRuntimeConfigSnapshot } from "./runtime-snapshot-BaQikjTR.js"; import "./operator-scopes-Dw7Gu2cA.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { n as GatewayErrorDetailCodes, t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Kn as validatePollParams, an as validateMessageActionParams, yr as validateSendParams } from "./src-BiL5aQto.js"; import "./message-channel-constants-2zSoJXQC.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import "./message-channel-BQrhwUEA.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { t as DEDUPE_MAX } from "./server-constants-BrVEC7RW.js"; import { a as isAgentHarnessSessionKey, m as resolveMissingAgentHarnessSessionError } from "./agent-harness-session-key-BOz3yx0-.js"; import { a as actionHasTarget } from "./channel-target-CD7634hc.js"; import { r as resolveChannelThreadAddressing } from "./thread-addressing-D0QS0tJI.js"; import { n as OutboundDeliveryError } from "./deliver-types-Diy-VQKA.js"; import { n as getAgentScopedMediaLocalRoots } from "./local-roots-BphuGw-a.js"; import { t as resolveAgentScopedOutboundMediaAccess } from "./read-capability-Ba0RMAnC.js"; import { o as projectOutboundPayloadPlanForMirror, t as createOutboundPayloadPlan } from "./payloads-DC1mf-yF.js"; import { o as selectMessageActionRequesterIdentity } from "./message-action-turn-capability-CpL-pT1E.js"; import { t as buildOutboundSessionContext } from "./session-context-CpdGMrv6.js"; import { a as maybeResolveIdLikeTarget } from "./target-resolver-q_MBPTOw.js"; import { t as authorizeGatewaySessionCreation, u as resolveSandboxedSessionCreation } from "./operator-role-policy-wsr1DeJv.js"; import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js"; import { r as loadGatewaySessionEntry } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { c as hydrateAttachmentParamsForAction, g as cancelTerminalSourceReplyDelivery, h as beginTerminalSourceReplyDelivery, n as resolveImplicitMessageActionTarget, p as resolveAttachmentMediaPolicy, r as dispatchChannelMessageAction, v as mirrorDeliveredSourceReplyToTranscript, y as reconcileTerminalSourceReplyDelivery } from "./message-action-normalization-CgPHpKwA.js"; import { r as resolveOutboundChannelPlugin } from "./channel-resolution-DfSowVxJ.js"; import { n as sendDurableMessageBatchCore } from "./send-Dk4bo3m5.js"; import "./runtime-zD0iWK04.js"; import { n as normalizePollInput } from "./polls-C-v11_tu.js"; import { r as resolveMessageChannelSelection } from "./channel-selection-DGYiyd5S.js"; import { i as resolveChannelDefaultAccountId } from "./helpers-VcK_WJ24.js"; import { r as validateExplicitMessageAccountSelection } from "./message-account-selection-1gun6iXS.js"; import { n as ensureOutboundSessionEntry, r as resolveOutboundSessionRoute } from "./outbound-session-n8tFMqOn.js"; import { a as resolveOutboundTarget } from "./targets-CP7C4mlG.js"; import { t as extractToolPayload } from "./tool-payload-C-ySaYQE.js"; import { i as isChannelPartialDeliveryError } from "./delivery-result-jWGDGh5u.js"; import { t as createOutboundSendDeps } from "./outbound-send-deps-CzQHPhLv.js"; import "./deps-CSNtqiEp.js"; import { t as formatForLog } from "./ws-log-Dkg9wKNU.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { n as createAgentRuntimeAuthorityGuard, r as hasActiveAgentRuntimeAuthority } from "./agent-runtime-authority-BkcmfdKo.js"; import { t as resolveGatewayPluginConfig } from "./runtime-plugin-config-CKZ9Aehs.js"; import { n as resolveGatewayInflightRequest$1, r as runGatewayInflightWork } from "./inflight-C7tVF6RA.js"; import { t as resolveGatewayConversationReadOrigin } from "./conversation-read-origin-CcxTNkzD.js"; //#region src/gateway/server-methods/send.ts const MESSAGE_OPERATION_ROUTE_BINDING_MAX = DEDUPE_MAX * 4; const messageOperationRouteBindings = /* @__PURE__ */ new WeakMap(); const messageOperationRouteBindingQueues = /* @__PURE__ */ new WeakMap(); function pruneMessageOperationRouteBindings(bindings, now) { for (const [key, entry] of bindings) if (!entry.retainUntilSettled && now - entry.ts > 3e5) bindings.delete(key); const excess = bindings.size - MESSAGE_OPERATION_ROUTE_BINDING_MAX; if (excess <= 0) return; const oldestSettledKeys = [...bindings.entries()].filter(([, entry]) => !entry.retainUntilSettled).toSorted(([, left], [, right]) => left.ts - right.ts).slice(0, excess).map(([key]) => key); for (const key of oldestSettledKeys) bindings.delete(key); } function getMessageOperationRouteBindings(context) { let bindings = messageOperationRouteBindings.get(context); if (!bindings) { bindings = /* @__PURE__ */ new Map(); messageOperationRouteBindings.set(context, bindings); } pruneMessageOperationRouteBindings(bindings, Date.now()); return bindings; } function getMessageOperationRouteBindingQueue(context) { let queue = messageOperationRouteBindingQueues.get(context); if (!queue) { queue = new KeyedAsyncQueue(); messageOperationRouteBindingQueues.set(context, queue); } return queue; } async function acquireMessageOperationRouteBindingLock(params) { if (!params.binding) return () => void 0; let signalAcquired; let signalRelease; const acquired = new Promise((resolve) => { signalAcquired = resolve; }); const held = new Promise((resolve) => { signalRelease = resolve; }); getMessageOperationRouteBindingQueue(params.context).enqueue(params.binding.key, async () => { signalAcquired?.(); await held; }); await acquired; let released = false; return () => { if (released) return; released = true; signalRelease?.(); }; } function resolveTrustedMessageActionToolContext(params) { const identity = params.client?.internal?.agentRuntimeIdentity; const messageActionContext = identity?.messageActionContext; if (!identity || !messageActionContext) return { ok: true, toolContext: void 0, ...selectMessageActionRequesterIdentity(void 0), sessionId: void 0, sourceReplySessionKey: void 0, sourceReplyFinal: void 0, sourceReplyToolCallId: void 0, runtimeAgentId: void 0 }; if (Date.now() >= messageActionContext.expiresAtMs) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "message.action agent runtime context has expired") }; const requestSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(params.request.sessionKey); const identitySessionKey = normalizeSessionKeyPreservingOpaquePeerIds(identity.sessionKey); const identityAgentId = normalizeAgentId(identity.agentId); const requestAgentId = normalizeOptionalString(params.request.agentId); const sessionAgentId = parseAgentSessionKey(requestSessionKey)?.agentId; const requestSessionId = normalizeOptionalString(params.request.sessionId); const sourceReplySessionKey = normalizeSessionKeyPreservingOpaquePeerIds(messageActionContext.sourceReplySessionKey) || void 0; const sourceReplySessionAgentId = parseAgentSessionKey(sourceReplySessionKey)?.agentId; if (!requestSessionKey || requestSessionKey !== identitySessionKey || requestAgentId && normalizeAgentId(requestAgentId) !== identityAgentId || sessionAgentId && normalizeAgentId(sessionAgentId) !== identityAgentId || messageActionContext.sessionId && requestSessionId !== messageActionContext.sessionId || sourceReplySessionKey && sourceReplySessionAgentId && normalizeAgentId(sourceReplySessionAgentId) !== identityAgentId) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "message.action agent runtime identity does not match the requested session") }; return { ok: true, toolContext: messageActionContext.toolContext, ...selectMessageActionRequesterIdentity(messageActionContext), sessionId: messageActionContext.sessionId, sourceReplySessionKey, sourceReplyFinal: messageActionContext.sourceReplyFinal, sourceReplyToolCallId: messageActionContext.sourceReplyToolCallId, runtimeAgentId: identityAgentId }; } function resolveMessageOperationAuthorityScope(params) { return params.prefix === "message.action" ? `:${params.conversationReadOrigin ?? "delegated"}` : ""; } function resolveGatewayInflightRequest(params) { const idem = params.idempotencyKey; const authorityScope = resolveMessageOperationAuthorityScope(params); const requestScope = params.requestScope ? `:${params.requestScope}` : ""; const dedupeKey = `${params.prefix}${authorityScope}${requestScope}:${idem}`; return resolveGatewayInflightRequest$1({ context: params.context, dedupeKey, idempotencyKey: idem, respond: params.respond }); } function parseMessageOperationRoute(requestScope) { if (!requestScope) return; try { const parsed = JSON.parse(requestScope); if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") return; const channel = normalizeMessageChannel(parsed[0]); const accountId = normalizeOptionalAccountId(parsed[1]); if (!channel || channel !== parsed[0] || !accountId || accountId !== parsed[1]) return; return { channel, accountId, requestScope }; } catch { return; } } function resolveMessageOperationRouteBinding(params) { const rawChannel = readStringValue(params.requestChannel); const channel = rawChannel ? normalizeMessageChannel(rawChannel) : void 0; if (rawChannel && !channel) return; const normalizedAccountIds = params.accountIds.filter((value) => value !== void 0 && value !== null && (typeof value !== "string" || value.trim())).map((value) => typeof value === "string" ? normalizeOptionalAccountId(value) : void 0); if (normalizedAccountIds.some((accountId) => !accountId)) return; const distinctAccountIds = [...new Set(normalizedAccountIds)]; if (distinctAccountIds.length > 1) return; const accountId = distinctAccountIds[0]; const authorityScope = resolveMessageOperationAuthorityScope(params); const explicitRouteScope = JSON.stringify([channel ?? null, accountId ?? null]); const key = `${params.prefix}${authorityScope}:route-binding:${explicitRouteScope}:${params.idempotencyKey}`; return { key, reservedRoute: parseMessageOperationRoute(getMessageOperationRouteBindings(params.context).get(key)?.requestScope) }; } function bindMessageOperationRoute(params) { if (!params.binding) return true; const bindings = getMessageOperationRouteBindings(params.context); const existing = bindings.get(params.binding.key); if (existing) { if (existing.requestScope !== params.requestScope) return false; bindings.set(params.binding.key, { ...existing, ts: Date.now() }); return true; } bindings.set(params.binding.key, { ts: Date.now(), requestScope: params.requestScope, retainUntilSettled: false }); pruneMessageOperationRouteBindings(bindings, Date.now()); return true; } function refreshMessageOperationRouteBinding(params) { if (!params.binding) return; const bindings = getMessageOperationRouteBindings(params.context); const existing = bindings.get(params.binding.key); if (existing?.requestScope === params.requestScope) { bindings.set(params.binding.key, { ...existing, ts: Date.now(), retainUntilSettled: false }); pruneMessageOperationRouteBindings(bindings, Date.now()); } } function retainMessageOperationRouteBinding(params) { if (!params.binding) return; const bindings = getMessageOperationRouteBindings(params.context); const existing = bindings.get(params.binding.key); if (existing?.requestScope === params.requestScope) bindings.set(params.binding.key, { ...existing, retainUntilSettled: true }); } function replayReservedMessageOperationRoute(params) { if (!params.binding?.reservedRoute) return; const inflight = resolveGatewayInflightRequest({ context: params.context, prefix: params.prefix, idempotencyKey: params.idempotencyKey, respond: params.respond, conversationReadOrigin: params.conversationReadOrigin, requestScope: params.binding.reservedRoute.requestScope }); if (inflight.kind === "ready") return; return inflight.done; } function resolveMessageOperationAccountRoute(params) { const accountIds = params.accountIds.map((accountId) => validateExplicitMessageAccountSelection({ cfg: params.cfg, channel: params.channel, accountId, plugin: params.plugin })).filter((accountId) => accountId !== void 0); const distinctAccountIds = [...new Set(accountIds)]; if (distinctAccountIds.length > 1) throw new Error(params.conflictMessage); const accountId = distinctAccountIds[0]; const effectiveAccountId = accountId ?? normalizeAccountId(resolveChannelDefaultAccountId({ plugin: params.plugin, cfg: params.cfg })); return { accountId, requestScope: JSON.stringify([params.channel, effectiveAccountId]) }; } async function withMessageOperationRoute(params) { const bindingParams = { context: params.context, prefix: params.prefix, idempotencyKey: params.idempotencyKey, conversationReadOrigin: params.conversationReadOrigin, requestChannel: params.requestChannel, accountIds: params.bindingAccountIds }; let binding = resolveMessageOperationRouteBinding(bindingParams); const releaseLock = await acquireMessageOperationRouteBindingLock({ context: params.context, binding }); try { binding = resolveMessageOperationRouteBinding(bindingParams); const reservedReplay = replayReservedMessageOperationRoute({ context: params.context, binding, prefix: params.prefix, idempotencyKey: params.idempotencyKey, respond: params.respond, conversationReadOrigin: params.conversationReadOrigin }); if (reservedReplay) { releaseLock(); await reservedReplay; return; } const resolved = await params.resolveChannel(binding?.reservedRoute?.channel ?? params.requestChannel); if (!resolved) return; let accountRoute; try { accountRoute = resolveMessageOperationAccountRoute({ ...resolved, accountIds: params.routeAccountIds(binding), conflictMessage: params.conflictMessage }); } catch (error) { respondGatewayInvalidRequest({ respond: params.respond, channel: resolved.channel, error }); return; } if (!bindMessageOperationRoute({ context: params.context, binding, requestScope: accountRoute.requestScope })) { respondGatewayInvalidRequest({ respond: params.respond, channel: resolved.channel, error: "idempotency key is already bound to a different message route" }); return; } const inflight = resolveGatewayInflightRequest({ context: params.context, prefix: params.prefix, idempotencyKey: params.idempotencyKey, respond: params.respond, conversationReadOrigin: params.conversationReadOrigin, requestScope: accountRoute.requestScope }); if (inflight.kind === "handled") { releaseLock(); await inflight.done; return; } if (params.authorize && !params.authorize()) { params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "agent runtime authority is no longer active")); return; } retainMessageOperationRouteBinding({ context: params.context, binding, requestScope: accountRoute.requestScope }); const work = params.work({ ...resolved, accountId: accountRoute.accountId, idem: inflight.idem, dedupeKey: inflight.dedupeKey, authorize: params.authorize ?? (() => true) }).finally(() => { refreshMessageOperationRouteBinding({ context: params.context, binding, requestScope: accountRoute.requestScope }); }); const inflightWork = runGatewayInflightWork({ ...inflight, work, respond: params.respond }); releaseLock(); await inflightWork; } finally { releaseLock(); } } function respondGatewayInvalidRequest(params) { params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, String(params.error)), { channel: params.channel, error: formatForLog(params.error) }); } async function resolveRequestedChannel(params) { const channelInput = readStringValue(params.requestChannel); const normalizedChannel = channelInput ? normalizeMessageChannel(channelInput) : void 0; if (params.rejectWebchatAsInternalOnly && normalizedChannel === "webchat") return { error: errorShape(ErrorCodes.INVALID_REQUEST, "unsupported channel: webchat (internal-only). Use `chat.send` for WebChat UI messages or choose a deliverable channel.") }; if (channelInput && !normalizedChannel) return { error: errorShape(ErrorCodes.INVALID_REQUEST, params.unsupportedMessage(channelInput)) }; const sourceCfg = params.context.getRuntimeConfig(); const cfg = resolveGatewayPluginConfig({ config: sourceCfg }); let channel = normalizedChannel; if (!channel) try { channel = (await resolveMessageChannelSelection({ cfg })).channel; } catch (err) { return { error: errorShape(ErrorCodes.INVALID_REQUEST, String(err)) }; } return { cfg, sourceCfg, channel }; } async function resolveInternalDeliveryChannel(requestChannel, context) { const resolvedChannel = await resolveRequestedChannel({ requestChannel, unsupportedMessage: (input) => `unsupported channel: ${input}`, context, rejectWebchatAsInternalOnly: true }); if ("error" in resolvedChannel) return { kind: "failed", result: { ok: false, error: resolvedChannel.error } }; return { kind: "ready", ...resolvedChannel }; } function resolveGatewayOutboundTarget(params) { const resolved = resolveOutboundTarget({ channel: params.channel, to: params.to, cfg: params.cfg, accountId: params.accountId, mode: "explicit" }); if (!resolved.ok) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, String(resolved.error)) }; return { ok: true, to: resolved.to }; } function resolveMessageActionRuntimeConfig(params) { const runtimeConfig = getRuntimeConfigSnapshot(); const runtimeSourceConfig = getRuntimeConfigSourceSnapshot(); if (!runtimeConfig || !runtimeSourceConfig) return params.cfg; const selected = selectApplicableRuntimeConfig({ inputConfig: params.sourceCfg, runtimeConfig, runtimeSourceConfig }); if (selected === runtimeConfig && selected !== params.cfg) return resolveGatewayPluginConfig({ config: selected }); return params.cfg; } function buildGatewayDeliveryPayload(params) { const payload = { runId: params.runId, messageId: params.result.messageId, channel: params.channel }; for (const key of [ "chatId", "channelId", "toJid", "conversationId", "pollId" ]) if (key in params.result) payload[key] = params.result[key]; return payload; } function createGatewayInflightResult(params) { params.context.dedupe.set(params.dedupeKey, { ts: Date.now(), ...params.result }); return { ...params.result, meta: { channel: params.channel, ...params.meta } }; } function createGatewayInflightSuccess(params) { return createGatewayInflightResult({ ...params, result: { ok: true, payload: params.payload } }); } function createGatewayInflightUnavailableFailure(params) { const partialDelivery = isChannelPartialDeliveryError(params.err) ? params.err.deliveryResult : void 0; const queuedDelivery = !partialDelivery && params.err instanceof OutboundDeliveryError && params.err.recoveryOwnedRetry === true; const error = errorShape(ErrorCodes.UNAVAILABLE, String(params.err), partialDelivery ? { details: { partialDelivery }, retryable: false } : queuedDelivery ? { details: { code: GatewayErrorDetailCodes.OUTBOUND_DELIVERY_QUEUED } } : void 0); return createGatewayInflightResult({ ...params, result: { ok: false, error }, meta: { error: formatForLog(params.err) } }); } function createGatewayInflightAuthorityFailure(params) { return createGatewayInflightResult({ ...params, result: { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "agent runtime authority is no longer active") } }); } async function mirrorDeliveredSourceReplyToTranscriptBestEffort(params) { try { if (!await mirrorDeliveredSourceReplyToTranscript(params.mirror) && params.mirror.sourceReplyFinal === true) params.context.logGateway?.warn?.("Terminal source reply receipt was not mirrored; restart recovery is fail-closed.", { channel: params.mirror.channel, sessionKey: params.mirror.sessionKey }); } catch (err) { params.context.logGateway?.warn?.("Source reply transcript mirror failed after delivery.", { error: formatForLog(err), channel: params.mirror.channel, sessionKey: params.mirror.sessionKey }); } } const sourceReplyTranscriptMirrorQueue = new KeyedAsyncQueue(); function resolveSourceReplyTranscriptMirrorQueueKey(mirror) { return mirror.sessionKey?.trim() || "__global__"; } function scheduleDeliveredSourceReplyTranscriptMirror(params) { const queueKey = resolveSourceReplyTranscriptMirrorQueueKey(params.mirror); return sourceReplyTranscriptMirrorQueue.enqueue(queueKey, () => mirrorDeliveredSourceReplyToTranscriptBestEffort(params)); } const sendHandlers = { "message.action": async ({ params, respond, context, client }) => { const p = params; if (!assertValidParams(p, validateMessageActionParams, "message.action", respond)) return; const request = p; const trustedContext = resolveTrustedMessageActionToolContext({ client, request }); if (!trustedContext.ok) { respond(false, void 0, trustedContext.error); return; } const conversationReadOrigin = resolveGatewayConversationReadOrigin({ client, requestedOrigin: request.conversationReadOrigin }); const agentRuntimeAuthority = createAgentRuntimeAuthorityGuard(client, context, respond); await withMessageOperationRoute({ context, prefix: "message.action", idempotencyKey: request.idempotencyKey, respond, conversationReadOrigin, requestChannel: request.channel, bindingAccountIds: [request.accountId, request.params.accountId], routeAccountIds: (binding) => [ request.accountId, request.params.accountId, binding?.reservedRoute?.accountId ], conflictMessage: "message.action accountId does not match params.accountId", authorize: agentRuntimeAuthority.hasActive, resolveChannel: async (requestChannel) => { const resolved = await resolveRequestedChannel({ requestChannel, unsupportedMessage: (input) => `unsupported channel: ${input}`, context, rejectWebchatAsInternalOnly: true }); if ("error" in resolved) { respond(false, void 0, resolved.error); return; } const { cfg: selectedCfg, sourceCfg, channel } = resolved; const cfg = resolveMessageActionRuntimeConfig({ cfg: selectedCfg, sourceCfg }); const plugin = resolveOutboundChannelPlugin({ channel, cfg }); const canonicalAction = (request.action === "send" && Boolean(plugin?.message?.send?.text || plugin?.outbound?.sendText) || request.action === "poll" && Boolean(plugin?.outbound?.sendPoll)) && (!plugin?.actions?.handleAction || plugin.actions.supportsAction?.({ action: request.action }) === false); if (!plugin || !plugin.actions?.handleAction && !canonicalAction) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Channel ${channel} does not support action ${request.action}.`)); return; } return { cfg, channel, plugin, canonicalAction }; }, work: async ({ cfg, channel, plugin, canonicalAction, accountId, dedupeKey, authorize }) => { try { const sessionKey = normalizeOptionalString(request.sessionKey) ?? void 0; const requestedAgentId = normalizeOptionalString(request.agentId) ?? trustedContext.runtimeAgentId; const sessionOwner = sessionKey ? resolveRequestedSessionAgentId(cfg, sessionKey, requestedAgentId) : void 0; if (sessionOwner && !sessionOwner.ok) return { ok: false, error: sessionOwner.error, meta: { channel } }; const agentId = sessionOwner?.agentId ?? requestedAgentId; const sourceReplySessionKey = trustedContext.sourceReplySessionKey; const sourceReplyOwner = sourceReplySessionKey ? resolveRequestedSessionAgentId(cfg, sourceReplySessionKey, agentId) : void 0; if (sourceReplyOwner && !sourceReplyOwner.ok) return { ok: false, error: sourceReplyOwner.error, meta: { channel } }; if (request.action === "send" && cfg.gateway?.roles) { const actionAgent = agentId ?? sourceReplyOwner?.agentId ?? resolveRequestedSessionAgentId(cfg, "main"); if (typeof actionAgent !== "string" && !actionAgent.ok) return { ok: false, error: actionAgent.error, meta: { channel } }; const actionAgentId = typeof actionAgent === "string" ? actionAgent : actionAgent.agentId; const agentAccessError = authorizeGatewaySessionCreation({ cfg, client, agentId: actionAgentId }); if (agentAccessError) return { ok: false, error: agentAccessError, meta: { channel } }; } if (accountId) request.params.accountId = accountId; if (canonicalAction && request.action === "send" && !normalizeOptionalString(request.params.target) && !actionHasTarget("send", request.params, { channel }) && !resolveImplicitMessageActionTarget(trustedContext.toolContext)) { const target = resolveOutboundTarget({ channel, plugin, cfg, accountId }); if (!target.ok) throw target.error; request.params.to = target.to; } const resolvedMediaAccess = resolveAgentScopedOutboundMediaAccess({ cfg, agentId, sessionKey, messageProvider: sessionKey ? void 0 : channel, accountId: sessionKey ? trustedContext.requesterAccountId ?? accountId : accountId, requesterSenderId: trustedContext.requesterSenderId, requesterSenderName: trustedContext.requesterSenderName, requesterSenderUsername: trustedContext.requesterSenderUsername, requesterSenderE164: trustedContext.requesterSenderE164 }); const mediaAccess = { localRoots: resolvedMediaAccess.localRoots, ...resolvedMediaAccess.workspaceDir ? { workspaceDir: resolvedMediaAccess.workspaceDir } : {} }; if (request.action === "send") await hydrateAttachmentParamsForAction({ cfg, channel, accountId, args: request.params, action: "send", mediaPolicy: resolveAttachmentMediaPolicy({ mediaAccess: resolvedMediaAccess }) }); const sourceReplyMirror = { action: request.action, channel, actionParams: request.params, cfg, accountId, currentAccountId: trustedContext.requesterAccountId, sessionKey: sourceReplySessionKey ?? sessionKey, sessionId: trustedContext.sessionId, agentId, toolContext: trustedContext.toolContext, replyToIsExplicit: request.reply?.source === "explicit", idempotencyKey: request.idempotencyKey, toolCallId: trustedContext.sourceReplyToolCallId, ...trustedContext.sourceReplyFinal !== void 0 ? { sourceReplyFinal: trustedContext.sourceReplyFinal } : {} }; const terminalDeliveryStart = trustedContext.sourceReplyFinal === true ? await beginTerminalSourceReplyDelivery(sourceReplyMirror) : void 0; if (terminalDeliveryStart && "outcome" in terminalDeliveryStart) return createGatewayInflightSuccess({ context, dedupeKey, payload: terminalDeliveryStart.result, channel }); const terminalDeliveryReceipt = terminalDeliveryStart; if (!authorize()) { await cancelTerminalSourceReplyDelivery(terminalDeliveryReceipt); return createGatewayInflightAuthorityFailure({ context, dedupeKey, channel }); } const gatewayClientScopes = client?.connect?.scopes ?? []; const inboundEventKind = request.inboundTurnKind === "room_event" ? "room_event" : "user_request"; const actionContext = { channel, action: request.action, cfg, params: request.params, reply: request.reply, accountId, deliveryRetryOwner: trustedContext.runtimeAgentId ? "caller" : void 0, ...selectMessageActionRequesterIdentity(trustedContext), senderIsOwner: gatewayClientScopes.includes("operator.admin") ? request.senderIsOwner === true : false, conversationReadOrigin, sessionKey, sessionId: normalizeOptionalString(request.sessionId) ?? void 0, inboundEventKind, agentId, mediaAccess, mediaLocalRoots: mediaAccess.localRoots, toolContext: trustedContext.toolContext, dryRun: false, gatewayClientScopes }; let payload; if (canonicalAction) { const { runMessageAction } = await import("./message-action-runner-CAny-Fw5.js"); payload = (await runMessageAction({ ...actionContext, gatewayOwnedDelivery: true, ...request.action === "send" ? { suppressTranscriptMirror: true, actionOrigin: trustedContext.runtimeAgentId ? "message-tool" : void 0, skipQueue: client?.internal?.agentRuntimeIdentity !== void 0, onPlatformSendDispatch: async () => agentRuntimeAuthority.commitGuard?.() } : {}, params: { ...request.params, channel, ...accountId ? { accountId } : {}, idempotencyKey: request.idempotencyKey } })).payload; } else { const handled = await dispatchChannelMessageAction(actionContext); if (handled) payload = extractToolPayload(handled); else { await cancelTerminalSourceReplyDelivery(terminalDeliveryReceipt); return createGatewayInflightResult({ context, dedupeKey, channel, result: { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `Message action ${request.action} not supported for channel ${channel}.`) } }); } } try { await reconcileTerminalSourceReplyDelivery({ deliveredPayload: payload, mirror: sourceReplyMirror, receipt: terminalDeliveryReceipt }); } catch (err) { context.logGateway?.warn?.("Terminal source reply receipt reconciliation failed.", { error: formatForLog(err), channel, sessionKey }); } await scheduleDeliveredSourceReplyTranscriptMirror({ context, mirror: { ...sourceReplyMirror, deliveredPayload: payload } }); return createGatewayInflightSuccess({ context, dedupeKey, payload, channel }); } catch (err) { if (!authorize()) return createGatewayInflightAuthorityFailure({ context, dedupeKey, channel }); return createGatewayInflightUnavailableFailure({ context, dedupeKey, channel, err }); } } }); }, send: async ({ params, respond, context, client }) => { const p = params; if (!assertValidParams(p, validateSendParams, "send", respond)) return; const request = p; const to = normalizeOptionalString(request.to) ?? ""; const message = request.message?.trim() ? request.message : ""; const mediaUrl = normalizeOptionalString(request.mediaUrl); const mediaUrls = Array.isArray(request.mediaUrls) ? request.mediaUrls.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)) : void 0; const buffer = readStringValue(request.buffer); if (!message && !mediaUrl && (mediaUrls?.length ?? 0) === 0 && !buffer) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid send params: text or media is required")); return; } const requestedAccountId = normalizeOptionalString(request.accountId); const replyToId = normalizeOptionalString(request.replyToId); const threadId = normalizeOptionalString(request.threadId); const agentRuntimeAuthority = createAgentRuntimeAuthorityGuard(client, context, respond); const hasAgentRuntimeAuthority = client?.internal?.agentRuntimeIdentity !== void 0; const commitAgentRuntimeAuthority = agentRuntimeAuthority.commitGuard; const onPlatformSendDispatch = commitAgentRuntimeAuthority ? async () => commitAgentRuntimeAuthority() : void 0; await withMessageOperationRoute({ context, prefix: "send", idempotencyKey: request.idempotencyKey, respond, requestChannel: request.channel, bindingAccountIds: [request.accountId], routeAccountIds: (binding) => [requestedAccountId, binding?.reservedRoute?.accountId], conflictMessage: "send account selections do not match", authorize: agentRuntimeAuthority.hasActive, resolveChannel: async (requestChannel) => { const resolved = await resolveInternalDeliveryChannel(requestChannel, context); if (resolved.kind !== "ready") { const result = resolved.result; respond(result.ok, result.payload, result.error, result.meta); return; } const { cfg, channel } = resolved; const plugin = resolveOutboundChannelPlugin({ channel, cfg }); if (!plugin) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported channel: ${channel}`)); return; } return { cfg, channel, plugin }; }, work: async ({ cfg, channel, accountId, idem, dedupeKey, authorize }) => { try { const resolvedTarget = resolveGatewayOutboundTarget({ channel, to, cfg, accountId }); if (!resolvedTarget.ok) return { ok: false, error: resolvedTarget.error, meta: { channel } }; const idLikeTarget = await maybeResolveIdLikeTarget({ cfg, channel, input: resolvedTarget.to, accountId }); const deliveryTarget = idLikeTarget?.to ?? resolvedTarget.to; const providedSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(request.sessionKey) || void 0; const explicitAgentId = normalizeOptionalString(request.agentId); const sessionOwner = providedSessionKey ? resolveRequestedSessionAgentId(cfg, providedSessionKey, explicitAgentId) : void 0; if (sessionOwner && !sessionOwner.ok) return { ok: false, error: sessionOwner.error, meta: { channel } }; const sessionAgentId = sessionOwner?.agentId; const implicitAgent = !explicitAgentId && !sessionAgentId ? resolveRequestedSessionAgentId(cfg, "main") : void 0; if (implicitAgent && !implicitAgent.ok) return { ok: false, error: implicitAgent.error, meta: { channel } }; const effectiveAgentId = explicitAgentId ?? sessionAgentId ?? (implicitAgent?.ok ? implicitAgent.agentId : null); if (!effectiveAgentId) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "agent selection is required"), meta: { channel } }; const sendArgs = { mediaUrl, mediaUrls, buffer, filename: normalizeOptionalString(request.filename) ?? void 0, contentType: normalizeOptionalString(request.contentType) ?? void 0 }; await hydrateAttachmentParamsForAction({ cfg, channel, accountId, args: sendArgs, action: "send", mediaPolicy: resolveAttachmentMediaPolicy({ mediaLocalRoots: getAgentScopedMediaLocalRoots(cfg, effectiveAgentId) }) }); const hydratedMediaUrl = normalizeOptionalString(sendArgs.mediaUrl); const hydratedMediaUrls = Array.isArray(sendArgs.mediaUrls) ? sendArgs.mediaUrls.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)) : void 0; const outboundDeps = context.deps ? createOutboundSendDeps(context.deps) : void 0; const outboundPayloads = [{ text: message, mediaUrl: hydratedMediaUrl, mediaUrls: hydratedMediaUrls, ...request.asVoice === true ? { audioAsVoice: true } : {} }]; const outboundPayloadPlan = createOutboundPayloadPlan(outboundPayloads); const mirrorProjection = projectOutboundPayloadPlanForMirror(outboundPayloadPlan); const mirrorText = mirrorProjection.text; const mirrorMediaUrls = mirrorProjection.mediaUrls; const derivedRoute = await resolveOutboundSessionRoute({ cfg, channel, agentId: effectiveAgentId, accountId, target: deliveryTarget, currentSessionKey: providedSessionKey, resolvedTarget: idLikeTarget, replyToId, threadId }); const providedSessionBaseKey = parseThreadSessionSuffix(providedSessionKey).baseSessionKey ?? providedSessionKey; const shouldUseDerivedThreadSessionKey = resolveChannelThreadAddressing(channel) === "message" && Boolean(providedSessionKey) && Boolean(normalizeOptionalString(derivedRoute?.threadId)) && normalizeOptionalLowercaseString(derivedRoute?.baseSessionKey) === normalizeOptionalLowercaseString(providedSessionBaseKey) && normalizeOptionalLowercaseString(derivedRoute?.sessionKey) !== providedSessionKey; const outboundRoute = derivedRoute ? providedSessionKey ? shouldUseDerivedThreadSessionKey ? { ...derivedRoute, baseSessionKey: derivedRoute.baseSessionKey ?? providedSessionKey } : { ...derivedRoute, sessionKey: providedSessionKey, baseSessionKey: providedSessionKey } : derivedRoute : null; const outboundSessionKey = outboundRoute?.sessionKey ?? providedSessionKey; if (outboundSessionKey) { const agentAccessError = authorizeGatewaySessionCreation({ cfg, client, agentId: effectiveAgentId }); if (agentAccessError) return { ok: false, error: agentAccessError, meta: { channel } }; } if (outboundSessionKey && isAgentHarnessSessionKey(outboundSessionKey)) { const { canonicalKey, entry } = loadGatewaySessionEntry(outboundSessionKey); const missingHarnessSessionError = resolveMissingAgentHarnessSessionError(canonicalKey, entry); if (missingHarnessSessionError) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, missingHarnessSessionError), meta: { channel } }; } let outboundRoutePersisted = false; const commitOutboundSessionRoute = async () => { if (outboundRoutePersisted || !outboundRoute) return; outboundRoutePersisted = true; await ensureOutboundSessionEntry({ cfg, channel, accountId, route: outboundRoute, creation: resolveSandboxedSessionCreation(client, cfg), sourceSessionKey: client?.internal?.agentRuntimeIdentity?.sessionKey }); }; const outboundSession = buildOutboundSessionContext({ cfg, agentId: effectiveAgentId, sessionKey: outboundSessionKey, conversationType: outboundRoute?.chatType }); if (!authorize()) return createGatewayInflightAuthorityFailure({ context, dedupeKey, channel }); const send = await sendDurableMessageBatchCore({ cfg, channel, to: deliveryTarget, accountId, payloads: outboundPayloads, replyToId: replyToId ?? null, session: outboundSession, gifPlayback: request.gifPlayback, forceDocument: request.forceDocument, threadId: outboundRoute?.threadId ?? threadId ?? null, deps: outboundDeps, gatewayClientScopes: client?.connect?.scopes ?? [], silent: request.silent, formatting: request.parseMode ? { parseMode: request.parseMode } : void 0, onDeliveryResult: commitOutboundSessionRoute, onPlatformSendDispatch, skipQueue: hasAgentRuntimeAuthority, mirror: outboundSessionKey ? { sessionKey: outboundSessionKey, agentId: effectiveAgentId, text: mirrorText || message, mediaUrls: mirrorMediaUrls.length > 0 ? mirrorMediaUrls : void 0, idempotencyKey: idem } : void 0 }); if (send.status === "sent" || send.status === "partial_failed") await commitOutboundSessionRoute(); if (send.status === "failed" || send.status === "partial_failed") throw send.error; const result = (send.status === "sent" ? send.results : []).at(-1); if (!result) throw new Error("No delivery result"); return createGatewayInflightSuccess({ context, dedupeKey, payload: buildGatewayDeliveryPayload({ runId: idem, channel, result }), channel }); } catch (err) { if (hasAgentRuntimeAuthority && !agentRuntimeAuthority.hasActive()) return createGatewayInflightAuthorityFailure({ context, dedupeKey, channel }); return createGatewayInflightUnavailableFailure({ context, dedupeKey, channel, err }); } } }); }, poll: async ({ params, respond, context, client }) => { const p = params; if (!assertValidParams(p, validatePollParams, "poll", respond)) return; const request = p; await withMessageOperationRoute({ context, prefix: "poll", idempotencyKey: request.idempotencyKey, respond, requestChannel: request.channel, bindingAccountIds: [request.accountId], routeAccountIds: (binding) => [request.accountId, binding?.reservedRoute?.accountId], conflictMessage: "poll account selections do not match", authorize: () => hasActiveAgentRuntimeAuthority(client, context), resolveChannel: async (requestChannel) => { const resolved = await resolveRequestedChannel({ requestChannel, unsupportedMessage: (input) => `unsupported poll channel: ${input}`, context }); if ("error" in resolved) { respond(false, void 0, resolved.error); return; } const { cfg, channel } = resolved; const plugin = resolveOutboundChannelPlugin({ channel, cfg }); const outbound = plugin?.outbound; if (typeof request.durationSeconds === "number" && outbound?.supportsPollDurationSeconds !== true) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `durationSeconds is not supported for ${channel} polls`)); return; } if (typeof request.isAnonymous === "boolean" && outbound?.supportsAnonymousPolls !== true) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `isAnonymous is not supported for ${channel} polls`)); return; } if (!plugin || !outbound?.sendPoll) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported poll channel: ${channel}`)); return; } return { cfg, channel, plugin, outbound, sendPoll: outbound.sendPoll }; }, work: async ({ cfg, channel, accountId, idem, dedupeKey, authorize, outbound, sendPoll }) => { const poll = { question: request.question, options: request.options, maxSelections: request.maxSelections, durationSeconds: request.durationSeconds, durationHours: request.durationHours }; const threadId = normalizeOptionalString(request.threadId); try { const resolvedTarget = resolveGatewayOutboundTarget({ channel, to: request.to.trim(), cfg, accountId }); if (!resolvedTarget.ok) return { ok: false, error: resolvedTarget.error }; const normalized = outbound.pollMaxOptions ? normalizePollInput(poll, { maxOptions: outbound.pollMaxOptions }) : normalizePollInput(poll); if (!authorize()) return createGatewayInflightAuthorityFailure({ context, dedupeKey, channel }); return createGatewayInflightSuccess({ context, dedupeKey, payload: buildGatewayDeliveryPayload({ runId: idem, channel, result: await sendPoll({ cfg, to: resolvedTarget.to, poll: normalized, accountId, threadId, silent: request.silent, isAnonymous: request.isAnonymous, gatewayClientScopes: client?.connect?.scopes ?? [] }) }), channel }); } catch (err) { return createGatewayInflightUnavailableFailure({ context, dedupeKey, channel, err }); } } }); } }; //#endregion export { sendHandlers };