UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

375 lines (374 loc) 15.6 kB
import { l as normalizeStringEntries } from "../string-normalization-WNUDCpXX.js"; import { a as resolveChannelIngressState$1, s as decideChannelIngress } from "../message-access-mwxdTUGC.js"; //#region src/plugin-sdk/channel-ingress.ts /** Stable selectors for the ingress gates most plugin SDK callers inspect. */ const CHANNEL_INGRESS_GATE_SELECTORS = { command: { phase: "command", kind: "command" }, activation: { phase: "activation", kind: "mention" }, dmSender: { phase: "sender", kind: "dmSender" }, groupSender: { phase: "sender", kind: "groupSender" }, event: { phase: "event", kind: "event" } }; function defaultNormalize(value) { return value; } function normalizeMatchValue(value, normalize) { const normalized = normalize(value); return normalized == null ? null : normalized.trim() || null; } function resolveDangerous(dangerous, entry) { return typeof dangerous === "function" ? dangerous(entry) : dangerous; } function defaultIngressMatchKey(params) { return `${params.kind}:${params.value}`; } /** Find the first gate matching a selector in an ingress decision graph. */ function findChannelIngressGate(decision, selector) { return decision.graph.gates.find((gate) => gate.phase === selector.phase && gate.kind === selector.kind); } /** Find the sender gate for a DM or group ingress decision. */ function findChannelIngressSenderGate(decision, params) { return findChannelIngressGate(decision, params.isGroup ? CHANNEL_INGRESS_GATE_SELECTORS.groupSender : CHANNEL_INGRESS_GATE_SELECTORS.dmSender); } /** Find the command authorization gate in an ingress decision, when command policy ran. */ function findChannelIngressCommandGate(decision) { return findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.command); } /** Run base and command ingress decisions for both DM and group states. */ function decideChannelIngressBundle(params) { return { dm: decideChannelIngress(params.directState, params.basePolicy), group: decideChannelIngress(params.groupState, params.basePolicy), dmCommand: decideChannelIngress(params.directState, params.commandPolicy), groupCommand: decideChannelIngress(params.groupState, params.commandPolicy) }; } function projectGroupPolicy(gate) { const policy = gate?.sender?.policy; return policy === "open" || policy === "disabled" ? policy : "allowlist"; } function projectMentionFacts(gate) { const activation = gate?.activation; if (!activation?.hasMentionFacts) return; return { canDetectMention: activation.canDetectMention ?? false, wasMentioned: activation.wasMentioned ?? false, hasAnyMention: activation.hasAnyMention, implicitMentionKinds: activation.implicitMentionKinds ? [...activation.implicitMentionKinds] : void 0, requireMention: activation.requireMention, effectiveWasMentioned: activation.effectiveWasMentioned, shouldSkip: activation.shouldSkip }; } function projectDmDecision(decision, dmSender) { if (decision.decision === "pairing") return "pairing"; if (dmSender) return dmSender.allowed ? "allow" : "deny"; return decision.admission === "drop" ? "deny" : "allow"; } /** Project a full ingress decision graph into the legacy AccessFacts shape used by channels. */ function projectIngressAccessFacts(decision) { const command = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.command); const activation = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.activation); const dmSender = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.dmSender); const groupSender = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.groupSender); const event = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.event); return { dm: { decision: projectDmDecision(decision, dmSender), reason: dmSender?.reasonCode ?? decision.reasonCode, allowFrom: [], allowlist: dmSender?.allowlist }, group: { policy: projectGroupPolicy(groupSender), routeAllowed: !decision.graph.gates.some((gate) => gate.phase === "route" && gate.effect === "block-dispatch"), senderAllowed: groupSender?.allowed ?? dmSender?.allowed ?? false, allowFrom: [], requireMention: activation?.activation?.requireMention ?? false, allowlist: groupSender?.allowlist }, commands: command?.command ? { authorized: command.allowed, shouldBlockControlCommand: command.command.shouldBlockControlCommand, reasonCode: command.reasonCode, useAccessGroups: command.command.useAccessGroups, allowTextCommands: command.command.allowTextCommands, modeWhenAccessGroupsOff: command.command.modeWhenAccessGroupsOff, authorizers: [] } : void 0, event: event?.event ? { ...event.event, authorized: event.allowed, reasonCode: event.reasonCode } : void 0, mentions: projectMentionFacts(activation) }; } /** Convert an ingress graph decision plus any local side effect into channel turn admission. */ function mapChannelIngressDecisionToTurnAdmission(decision, sideEffect) { if (decision.admission === "dispatch") return { kind: "dispatch", reason: decision.reasonCode }; if (decision.admission === "observe") return { kind: "observeOnly", reason: decision.reasonCode }; if (decision.admission === "pairing-required") return sideEffect.kind === "pairing-reply-sent" ? { kind: "handled", reason: decision.reasonCode } : { kind: "drop", reason: decision.reasonCode }; if (decision.admission === "skip") return sideEffect.kind === "pending-history-recorded" || sideEffect.kind === "local-event-handled" || sideEffect.kind === "command-reply-sent" ? { kind: "handled", reason: decision.reasonCode } : { kind: "drop", reason: decision.reasonCode, recordHistory: false }; return sideEffect.kind === "local-event-handled" || sideEffect.kind === "command-reply-sent" ? { kind: "handled", reason: decision.reasonCode } : { kind: "drop", reason: decision.reasonCode }; } /** Brand a non-empty plugin id for channel ingress diagnostics and gate ids. */ function createChannelIngressPluginId(id) { const trimmed = id.trim(); if (!trimmed) throw new Error("Channel ingress plugin id must be non-empty."); return trimmed; } /** * Create a channel ingress subject from one or more identifiers. * Missing opaque ids are generated deterministically so redacted match output stays stable. */ function createChannelIngressSubject(input) { return { identifiers: ("identifiers" in input ? input.identifiers : [input]).map((identifier, index) => ({ opaqueId: identifier.opaqueId ?? `subject-${index + 1}`, kind: identifier.kind ?? "stable-id", value: identifier.value, dangerous: identifier.dangerous, sensitivity: identifier.sensitivity })) }; } /** * Create an adapter for channels that match allowlist entries against one normalized string id. * Wildcards are preserved as `*`; empty normalized values are omitted from matchable entries. */ function createChannelIngressStringAdapter(params = {}) { const kind = params.kind ?? "stable-id"; const normalizeEntry = params.normalizeEntry ?? defaultNormalize; const normalizeSubject = params.normalizeSubject ?? normalizeEntry; const isWildcardEntry = params.isWildcardEntry ?? ((entry) => entry === "*"); return { normalizeEntries({ entries }) { return { matchable: normalizeStringEntries(entries).flatMap((entry, index) => { const value = isWildcardEntry(entry) ? "*" : normalizeMatchValue(entry, normalizeEntry); if (!value) return []; return [{ opaqueEntryId: params.resolveEntryId?.({ entry, index }) ?? `entry-${index + 1}`, kind, value, dangerous: resolveDangerous(params.dangerous, entry), sensitivity: params.sensitivity }]; }), invalid: [], disabled: [] }; }, matchSubject({ subject, entries }) { const values = new Set(subject.identifiers.flatMap((identifier) => { if (identifier.kind !== kind) return []; const value = normalizeMatchValue(identifier.value, normalizeSubject); return value ? [value] : []; })); const matchedEntryIds = entries.filter((entry) => entry.kind === kind && (entry.value === "*" || values.has(entry.value))).map((entry) => entry.opaqueEntryId); return { matched: matchedEntryIds.length > 0, matchedEntryIds }; } }; } /** * Create an adapter for channels that match one allowlist entry against multiple identifier kinds. * This is useful when a channel supports stable ids plus aliases such as email or username. */ function createChannelIngressMultiIdentifierAdapter(params) { const getEntryMatchKey = params.getEntryMatchKey ?? defaultIngressMatchKey; const getSubjectMatchKeys = params.getSubjectMatchKeys ?? ((identifier) => [defaultIngressMatchKey(identifier)]); const isWildcardEntry = params.isWildcardEntry ?? ((entry) => entry.value === "*"); return { normalizeEntries({ entries }) { return { matchable: entries.flatMap((entry, index) => params.normalizeEntry(entry, index)), invalid: [], disabled: [] }; }, matchSubject({ subject, entries }) { const subjectKeys = new Set(subject.identifiers.flatMap((identifier) => getSubjectMatchKeys(identifier).filter((key) => Boolean(key)))); const matchedEntryIds = entries.filter((entry) => { if (isWildcardEntry(entry)) return true; const key = getEntryMatchKey(entry); return key ? subjectKeys.has(key) : false; }).map((entry) => entry.opaqueEntryId); return { matched: matchedEntryIds.length > 0, matchedEntryIds }; } }; } /** Exhaustiveness helper for switch statements over ingress reason codes. */ function assertNeverChannelIngressReason(reasonCode) { throw new Error(`Unhandled channel ingress reason code: ${String(reasonCode)}`); } /** * Read the sender gate reason code for legacy callers. * * @deprecated Use `senderAccess.reasonCode` from `resolveChannelMessageIngress(...)` or typed gate selectors. */ function findChannelIngressSenderReasonCode(decision, params) { return findChannelIngressSenderGate(decision, params)?.reasonCode ?? decision.reasonCode; } /** * Map channel-ingress reason codes back to legacy DM/group access reason codes. * * @deprecated Use `senderAccess.reasonCode` from `resolveChannelMessageIngress(...)`. */ function mapChannelIngressReasonCodeToDmGroupAccessReason(params) { switch (params.reasonCode) { case "group_policy_open": case "group_policy_allowed": return "group_policy_allowed"; case "group_policy_disabled": return "group_policy_disabled"; case "route_sender_empty": case "group_policy_empty_allowlist": return "group_policy_empty_allowlist"; case "group_policy_not_allowlisted": return "group_policy_not_allowlisted"; case "dm_policy_open": return "dm_policy_open"; case "dm_policy_disabled": return "dm_policy_disabled"; case "dm_policy_allowlisted": return "dm_policy_allowlisted"; case "dm_policy_pairing_required": return "dm_policy_pairing_required"; default: return params.isGroup ? "group_policy_not_allowlisted" : "dm_policy_not_allowlisted"; } } /** * Format a legacy DM/group policy reason string from a mapped ingress reason code. * * @deprecated Use `senderAccess.reason` from `resolveChannelMessageIngress(...)`. */ function formatChannelIngressPolicyReason(params) { switch (params.reasonCode) { case "group_policy_allowed": return `groupPolicy=${params.groupPolicy}`; case "group_policy_disabled": return "groupPolicy=disabled"; case "group_policy_empty_allowlist": return "groupPolicy=allowlist (empty allowlist)"; case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)"; case "dm_policy_open": return "dmPolicy=open"; case "dm_policy_disabled": return "dmPolicy=disabled"; case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy} (allowlisted)`; case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)"; case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy} (not allowlisted)`; } return params.reasonCode; } /** * Project a sender ingress reason into the legacy group-access compatibility shape. * * @deprecated Use `senderAccess.groupAccess` from `resolveChannelMessageIngress(...)`. */ function projectChannelIngressSenderGroupAccess(params) { const reasonCode = mapChannelIngressReasonCodeToDmGroupAccessReason({ reasonCode: params.reasonCode, isGroup: true }); const reason = params.groupPolicy === "disabled" || reasonCode === "group_policy_disabled" ? "disabled" : reasonCode === "group_policy_empty_allowlist" ? "empty_allowlist" : reasonCode === "group_policy_not_allowlisted" ? "sender_not_allowlisted" : "allowed"; return { allowed: reason === "allowed" && params.decisionAllowed, groupPolicy: params.groupPolicy, providerMissingFallbackApplied: params.providerMissingFallbackApplied ?? false, reason }; } /** * Project a full ingress decision into the legacy DM/group access compatibility shape. * * @deprecated Use `senderAccess` from `resolveChannelMessageIngress(...)`. */ function projectChannelIngressDmGroupAccess(params) { const reasonCode = mapChannelIngressReasonCodeToDmGroupAccessReason({ reasonCode: findChannelIngressSenderReasonCode(params.ingress, { isGroup: params.isGroup }), isGroup: params.isGroup }); return { decision: reasonCode === "dm_policy_pairing_required" ? "pairing" : params.ingress.decision === "allow" ? "allow" : "block", reasonCode, reason: formatChannelIngressPolicyReason({ reasonCode, dmPolicy: params.dmPolicy, groupPolicy: params.groupPolicy }) }; } /** Resolve and normalize channel ingress state from SDK input. */ async function resolveChannelIngressState(input) { return await resolveChannelIngressState$1(input); } /** * Resolve legacy ingress access with compatibility projections and effective allowlists. * * @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */ async function resolveChannelIngressAccess(params) { const { policy, effectiveAllowFrom, effectiveGroupAllowFrom, ...stateInput } = params; const state = await resolveChannelIngressState(stateInput); const ingress = decideChannelIngress(state, policy); const isGroup = params.conversation.kind !== "direct"; const senderReasonCode = findChannelIngressSenderReasonCode(ingress, { isGroup }); const access = projectChannelIngressDmGroupAccess({ ingress, isGroup, dmPolicy: policy.dmPolicy, groupPolicy: policy.groupPolicy }); const commandGate = findChannelIngressCommandGate(ingress); return { state, ingress, isGroup, senderReasonCode, access: { ...access, effectiveAllowFrom: [...effectiveAllowFrom ?? []], effectiveGroupAllowFrom: [...effectiveGroupAllowFrom ?? []] }, commandAuthorized: commandGate?.allowed === true, shouldBlockControlCommand: commandGate?.command?.shouldBlockControlCommand === true }; } //#endregion export { CHANNEL_INGRESS_GATE_SELECTORS, assertNeverChannelIngressReason, createChannelIngressMultiIdentifierAdapter, createChannelIngressPluginId, createChannelIngressStringAdapter, createChannelIngressSubject, decideChannelIngress, decideChannelIngressBundle, findChannelIngressCommandGate, findChannelIngressGate, findChannelIngressSenderGate, findChannelIngressSenderReasonCode, formatChannelIngressPolicyReason, mapChannelIngressDecisionToTurnAdmission, mapChannelIngressReasonCodeToDmGroupAccessReason, projectChannelIngressDmGroupAccess, projectChannelIngressSenderGroupAccess, projectIngressAccessFacts, resolveChannelIngressAccess, resolveChannelIngressState };