UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

464 lines (463 loc) 21.5 kB
import { t as pruneMapToMaxSize } from "../map-size-CNcWiFKu.js"; import { t as formatFencedCodeBlock } from "../markdown-code-Buzx6wvi.js"; import { n as sanitizeForPromptLiteral } from "../sanitize-for-prompt-C5q9LjmF.js"; import { n as summarizeApprovalScope } from "../approval-scope-BL54HpUv.js"; import { t as formatApprovalDisplayPath } from "../approval-display-paths-DlQSsCnq.js"; import { a as buildExecApprovalPendingReplyPayload, f as formatExecApprovalExpiresIn } from "../exec-approval-reply-ZdeEqz7J.js"; import { s as normalizeApprovalRequest } from "../approval-request-account-binding-Sc-eGYO-.js"; import { l as shouldSuppressLocalNativeExecApprovalPrompt } from "../approval-native-helpers-Bmx8YrQ2.js"; import { r as buildPluginApprovalPendingReplyPayload, t as buildApprovalPendingReplyPayload } from "../approval-renderers-DKlGmVsG.js"; import { n as buildPendingApprovalView } from "../approval-view-model-C4_4Iixz.js"; //#region src/plugin-sdk/approval-reaction-binding.ts /** Build the private marker revalidated after channel delivery. */ function buildApprovalReactionDeliveredBindingMarker(binding) { return { version: 1, ...binding }; } /** Read a nonempty, duplicate-free list without accepting unrecognized decisions. */ function readApprovalReactionDecisionList(value) { if (!Array.isArray(value) || value.length === 0) return null; const decisions = []; for (const decision of value) { if (decision !== "allow-once" && decision !== "allow-always" && decision !== "deny" || decisions.includes(decision)) return null; decisions.push(decision); } return decisions; } /** Normalize the shipped approval command spelling without accepting other decisions. */ function normalizeApprovalReactionDecision(value) { const normalized = value.trim().toLowerCase(); if (normalized === "always") return "allow-always"; return normalized === "allow-once" || normalized === "allow-always" || normalized === "deny" ? normalized : null; } /** Compare approved decision sets independently of presentation order. */ function approvalReactionDecisionSetsMatch(left, right) { return left.length === right.length && left.every((decision) => right.includes(decision)); } /** Validate canonical approval metadata without inferring its owner from text. */ function readApprovalReactionDeliveryMetadata(payload, options = {}) { const value = payload.channelData?.execApproval; if (!value || typeof value !== "object" || Array.isArray(value)) return null; const record = value; if (typeof record.approvalId !== "string") return null; const approvalId = options.trimApprovalId ? record.approvalId.trim() : record.approvalId; const approvalSlug = typeof record.approvalSlug === "string" ? record.approvalSlug.trim() : ""; const allowedDecisions = readApprovalReactionDecisionList(record.allowedDecisions); if (!approvalId || !options.trimApprovalId && approvalId !== approvalId.trim() || options.requireApprovalSlug && !approvalSlug || record.approvalKind !== "exec" && record.approvalKind !== "plugin" || !allowedDecisions) return null; return { approvalId, approvalKind: record.approvalKind, allowedDecisions, ...approvalSlug ? { approvalSlug } : {} }; } /** Verify that typed presentation controls exactly match authoritative approval metadata. */ function readApprovalReactionPresentationBinding(params) { const metadata = readApprovalReactionDeliveryMetadata(params.payload, params); if (!metadata) return null; const actions = (params.presentation ?? params.payload.presentation)?.blocks.flatMap((block) => block.type === "buttons" ? block.buttons.flatMap((button) => button.action?.type === "approval" ? [button.action] : []) : []); if (!actions?.length || actions.some((action) => action.approvalId !== metadata.approvalId || action.approvalKind !== metadata.approvalKind)) return null; const decisions = readApprovalReactionDecisionList(actions.map((action) => action.decision)); return decisions && approvalReactionDecisionSetsMatch(metadata.allowedDecisions, decisions) ? metadata : null; } /** Revalidate the private delivery marker against canonical typed approval metadata. */ function readApprovalReactionDeliveredBinding(params) { const metadata = readApprovalReactionDeliveryMetadata(params.payload, params); const marker = params.payload.channelData?.[params.channelDataKey]; if (!metadata || !marker || typeof marker !== "object" || Array.isArray(marker)) return null; const record = marker; const markerId = typeof record.approvalId === "string" && params.trimApprovalId ? record.approvalId.trim() : record.approvalId; const markerSlug = typeof record.approvalSlug === "string" ? record.approvalSlug.trim() : ""; const decisions = readApprovalReactionDecisionList(record.allowedDecisions); return record.version === 1 && markerId === metadata.approvalId && record.approvalKind === metadata.approvalKind && (!params.requireApprovalSlug || markerSlug === metadata.approvalSlug) && decisions && approvalReactionDecisionSetsMatch(metadata.allowedDecisions, decisions) ? metadata : null; } //#endregion //#region src/plugin-sdk/approval-reaction-runtime.ts /** Canonical reaction controls shown for approval prompts, in product display order. */ const APPROVAL_REACTION_BINDINGS = [ { decision: "allow-once", emoji: "👍", label: "Allow Once" }, { decision: "allow-always", emoji: "♾️", label: "Allow Always" }, { decision: "deny", emoji: "👎", label: "Deny" } ]; const APPROVAL_REACTION_ORDER = APPROVAL_REACTION_BINDINGS.map((binding) => binding.decision); const VARIATION_SELECTOR_RE = /[\uFE0E\uFE0F]/gu; const FITZPATRICK_MODIFIER_RE = /[\u{1F3FB}-\u{1F3FF}]/gu; function normalizeDecisionList(allowedDecisions) { const allowed = new Set(allowedDecisions); return APPROVAL_REACTION_ORDER.filter((decision) => allowed.has(decision)); } /** List the canonical reaction bindings allowed for a specific approval request. */ function listApprovalReactionBindings(params) { const allowed = new Set(normalizeDecisionList(params.allowedDecisions)); return APPROVAL_REACTION_BINDINGS.filter((binding) => allowed.has(binding.decision)).map((binding) => ({ decision: binding.decision, emoji: binding.emoji, label: binding.label })); } /** Build user-facing reaction instructions, or null when no reaction decisions are allowed. */ function buildApprovalReactionHint(params) { const bindings = listApprovalReactionBindings(params); if (bindings.length === 0) return null; return `React with:\n\n${bindings.map((binding) => `${binding.emoji} ${binding.label}`).join("\n")}`; } const APPROVAL_REACTION_HINT_PRESENT_RE = /(^|\n)React with:\s*(\n|$)/i; /** True when approval prompt text already carries a reaction hint block. */ function hasApprovalReactionHintText(text) { return APPROVAL_REACTION_HINT_PRESENT_RE.test(text ?? ""); } /** Inserts a reaction hint after the `ID: <id>` header line, else prepends it. */ function insertApprovalReactionHintNearIdHeader(params) { const lines = params.text.split(/\r?\n/); const idLineIndex = lines.findIndex((line) => /^ID:\s*\S+/.test(line.trim())); if (idLineIndex >= 0) { const before = lines.slice(0, idLineIndex + 1).join("\n"); const after = lines.slice(idLineIndex + 1).join("\n").replace(/^\n+/, ""); return after ? `${before}\n\n${params.hint}\n\n${after}` : `${before}\n\n${params.hint}`; } return `${params.hint}\n\n${params.text}`; } /** Adds the canonical reaction hint to approval prompt text unless one is present. */ function addApprovalReactionHintToText(params) { if (hasApprovalReactionHintText(params.text)) return params.text; const hint = buildApprovalReactionHint({ allowedDecisions: params.allowedDecisions }); return hint ? insertApprovalReactionHintNearIdHeader({ text: params.text, hint }) : params.text; } /** Normalize reaction emoji so skin-tone and text/presentation variants match canonical bindings. */ function normalizeApprovalReactionEmoji(reactionKey) { const normalized = reactionKey.trim().replace(VARIATION_SELECTOR_RE, "").replace(FITZPATRICK_MODIFIER_RE, ""); if (normalized === "♾") return "♾️"; return normalized; } /** Resolve a reaction key to an allowed approval decision. */ function resolveApprovalReactionDecision(params) { const normalizedEmoji = normalizeApprovalReactionEmoji(params.reactionKey); if (!normalizedEmoji) return null; for (const binding of listApprovalReactionBindings(params)) if (binding.emoji === normalizedEmoji) return { decision: binding.decision, normalizedEmoji }; return null; } function resolveApprovalReactionTargetInternal(params) { const target = params.target; if (!target) return null; const decision = resolveApprovalReactionDecision({ reactionKey: params.reactionKey, allowedDecisions: target.allowedDecisions }); if (!decision) return null; const approvalId = params.allowLegacyKindInference ? target.approvalId.trim() : target.approvalId; const approvalKind = target.approvalKind; if (!approvalId) return null; const resolvedKind = approvalKind === "exec" || approvalKind === "plugin" ? approvalKind : params.allowLegacyKindInference ? approvalId.startsWith("plugin:") ? "plugin" : "exec" : null; if (!resolvedKind) return null; return { approvalId, approvalKind: resolvedKind, decision: decision.decision, normalizedEmoji: decision.normalizedEmoji, ...target.route === void 0 ? {} : { route: target.route } }; } /** Resolve an explicitly typed target without deriving ownership from its id. */ function resolveTypedApprovalReactionTarget(params) { return resolveApprovalReactionTargetInternal({ ...params, allowLegacyKindInference: false }); } function formatSeverity(value) { return value === "critical" ? "Critical" : value === "info" ? "Info" : "Warning"; } function buildDecisionText(allowedDecisions) { return allowedDecisions.join("|"); } function buildManualInstructionSection(params) { const lines = []; if (!params.allowedDecisions.includes("allow-always")) lines.push(params.approvalKind === "exec" ? "Allow Always is unavailable for this command." : "Allow Always is unavailable because the effective policy requires approval every time."); if (params.allowedDecisions.length > 0) lines.push(`Reply with: /approve ${params.approvalId} ${buildDecisionText(params.allowedDecisions)}`); return lines; } function buildCommandActionInstructionSection(actions) { return actions.flatMap((action) => action.command.trim() ? [`${action.label}: ${action.command}`] : []); } function listDecisionActions(actions) { return normalizeDecisionList(actions.flatMap((action) => "decision" in action && action.decision ? [action.decision] : [])); } function buildApprovalReactionPromptText(params) { const { view } = params; const scopeSummary = view.scope ? summarizeApprovalScope(view.scope) : void 0; const allowedDecisions = listDecisionActions(view.actions); const sections = []; if (view.approvalKind === "exec") { const header = ["**Exec approval required**", `**ID:** ${view.approvalId}`]; sections.push(header.join("\n")); const warningText = view.warningText?.trim(); if (warningText) sections.push(`**${warningText}**`); const warningLines = view.commandAnalysis?.warningLines?.map((line) => line.trim()).filter(Boolean).slice(0, 5); if (warningLines?.length) sections.push(["**Command analysis:**", ...warningLines.map((line) => `- ${line}`)].join("\n")); sections.push(["**Pending command:**", formatFencedCodeBlock(view.commandText, "sh")].join("\n")); const info = []; if (view.cwd) info.push(`**CWD:** ${formatApprovalDisplayPath(sanitizeForPromptLiteral(view.cwd))}`); if (view.host) info.push(`**Host:** ${view.host}`); if (view.nodeId) info.push(`**Node:** ${view.nodeId}`); if (scopeSummary) info.push(`**Scope:** ${scopeSummary}`); if (view.agentId) info.push(`**Agent:** ${view.agentId}`); if (view.ask) info.push(`**Ask:** ${view.ask}`); info.push(`**Expires in:** ${formatExecApprovalExpiresIn(view.expiresAtMs, params.nowMs)}`); info.push(`**Full id:** \`${view.approvalId}\``); sections.push(info.join("\n")); } else if (view.approvalKind === "system-agent") { const details = ["**OpenClaw change requires approval**", `**Change:** ${view.operationSummary}`]; if (view.agentId) details.push(`**Agent:** ${view.agentId}`); details.push(`**Expires in:** ${formatExecApprovalExpiresIn(view.expiresAtMs, params.nowMs)}`); sections.push(details.join("\n")); } else { const header = ["**Plugin approval required**", `**ID:** ${view.approvalId}`]; sections.push(header.join("\n")); const details = [`**Title:** ${view.title}`]; if (view.description) details.push(`**Description:** ${view.description}`); if (scopeSummary) details.push(`**Scope:** ${scopeSummary}`); details.push(`**Severity:** ${formatSeverity(view.severity)}`); if (view.toolName) details.push(`**Tool:** ${view.toolName}`); if (view.pluginId) details.push(`**Plugin:** ${view.pluginId}`); if (view.agentId) details.push(`**Agent:** ${view.agentId}`); details.push(`**Expires in:** ${formatExecApprovalExpiresIn(view.expiresAtMs, params.nowMs)}`); details.push(`**Full id:** \`${view.approvalId}\``); sections.push(details.join("\n")); } if (params.reactionHint) sections.push(params.reactionHint); const commandInstructions = buildCommandActionInstructionSection(view.actions); if (commandInstructions.length > 0) sections.push(commandInstructions.join("\n")); const manualInstructions = buildManualInstructionSection({ approvalKind: view.approvalKind, approvalId: view.approvalId, allowedDecisions }); if (manualInstructions.length > 0) sections.push(manualInstructions.join("\n")); return sections.filter(Boolean).join("\n\n"); } function withoutPresentation(payload) { const { presentation: _presentation, interactive: _interactive, ...rest } = payload; return rest; } function buildMetadataPayload(params) { const sessionKey = params.request.request.sessionKey ?? null; return withoutPresentation(buildApprovalPendingReplyPayload({ approvalKind: params.view.approvalKind, approvalId: params.view.approvalId, approvalSlug: params.view.approvalId.slice(0, 8), text: params.text, agentId: params.view.agentId ?? null, allowedDecisions: params.allowedDecisions, sessionKey })); } /** Build an approval prompt payload with reaction bindings for a prepared view. */ function buildApprovalPendingPromptPayload(params) { const allowedDecisions = listDecisionActions(params.view.actions); const reactionBindings = listApprovalReactionBindings({ allowedDecisions }); const text = buildApprovalReactionPromptText({ view: params.view, nowMs: params.nowMs, reactionHint: buildApprovalReactionHint({ allowedDecisions }) }); return { ...buildMetadataPayload({ request: params.request, view: params.view, text, allowedDecisions }), allowedDecisions, reactionBindings }; } /** Build an approval prompt payload with reaction bindings directly from a request. */ function buildApprovalReactionPromptPayloadForRequest(params) { return buildApprovalPendingPromptPayload({ request: params.request, view: buildPendingApprovalView(params.request), nowMs: params.nowMs }); } function replaceApprovalIdPlaceholder(text, approvalId) { return (text ?? "").replace(/\/approve\s+<id>/g, `/approve ${approvalId}`); } /** Build reaction and manual-fallback pending approval content for a prepared view. */ function buildApprovalReactionPendingContent(params) { const reactionPayload = buildApprovalPendingPromptPayload(params); const request = normalizeApprovalRequest(params.request); if (params.view.approvalKind === "plugin") { if (request.approvalKind !== "plugin") throw new Error("approval request and view kinds do not match"); const payload = buildPluginApprovalPendingReplyPayload({ request, nowMs: params.nowMs, allowedDecisions: reactionPayload.allowedDecisions }); return { reactionPayload, manualFallbackPayload: withoutPresentation({ ...payload, text: replaceApprovalIdPlaceholder(payload.text, request.id) }) }; } if (params.view.approvalKind === "system-agent") { if (request.approvalKind !== "system-agent") throw new Error("approval request and view kinds do not match"); return { reactionPayload, manualFallbackPayload: withoutPresentation(buildApprovalPendingReplyPayload({ approvalKind: "system-agent", approvalId: request.id, approvalSlug: request.id.slice(0, 8), text: reactionPayload.text ?? "", agentId: params.view.agentId ?? null, allowedDecisions: reactionPayload.allowedDecisions, sessionKey: request.request.sessionKey ?? null })) }; } if (request.approvalKind !== "exec") throw new Error("approval request and view kinds do not match"); return { reactionPayload, manualFallbackPayload: withoutPresentation(buildExecApprovalPendingReplyPayload({ approvalId: request.id, approvalSlug: request.id.slice(0, 8), approvalCommandId: request.id, warningText: params.view.warningText ?? void 0, ask: params.view.ask ?? null, agentId: params.view.agentId ?? null, allowedDecisions: reactionPayload.allowedDecisions, command: params.view.commandText, cwd: params.view.cwd ?? void 0, host: params.view.host === "node" ? "node" : "gateway", nodeId: params.view.nodeId ?? void 0, scope: params.view.scope ?? void 0, sessionKey: params.view.sessionKey ?? null, expiresAtMs: request.expiresAtMs, nowMs: params.nowMs })) }; } /** * Prompt copy for channels whose native controls (Apple Messages polls, inline * buttons) own the decision surface. Same bold headers and labels as the * reaction prompt (#85954) minus the tapback hint, which would advertise a * second, redundant control path next to the native one. */ function buildApprovalNativeControlsPromptText(params) { return buildApprovalReactionPromptText({ view: params.view, nowMs: params.nowMs, reactionHint: null }); } /** Build reaction and manual-fallback pending approval content directly from a request. */ function buildApprovalReactionPendingContentForRequest(params) { return buildApprovalReactionPendingContent({ request: params.request, view: buildPendingApprovalView(params.request), nowMs: params.nowMs }); } /** Create an approval target store backed by memory with optional persistent storage. */ function createApprovalReactionTargetStore(params) { const nowMs = params.nowMs ?? (() => Date.now()); const memory = /* @__PURE__ */ new Map(); let persistentStore; let persistentStoreDisabled = false; const disablePersistentStore = (error) => { persistentStoreDisabled = true; persistentStore = void 0; params.logPersistentError?.(error); }; const getPersistentStore = () => { if (persistentStoreDisabled || !params.openStore) return; if (persistentStore) return persistentStore; try { persistentStore = params.openStore({ namespace: params.namespace, maxEntries: params.maxEntries, defaultTtlMs: params.defaultTtlMs }); return persistentStore; } catch (error) { disablePersistentStore(error); return; } }; const pruneMemory = () => { const now = nowMs(); for (const [key, entry] of memory) if (entry.expiresAtMs <= now) memory.delete(key); pruneMapToMaxSize(memory, params.maxEntries); }; return { register(key, target, opts) { const normalizedKey = key.trim(); if (!normalizedKey) return; const ttlMs = Math.max(1, opts?.ttlMs ?? params.defaultTtlMs); memory.set(normalizedKey, { target, expiresAtMs: nowMs() + ttlMs }); pruneMemory(); const store = getPersistentStore(); if (!store) return; store.register(normalizedKey, { version: 1, target }, { ttlMs }).catch(disablePersistentStore); }, async lookup(key) { const normalizedKey = key.trim(); if (!normalizedKey) return null; pruneMemory(); const entry = memory.get(normalizedKey); if (entry) return entry.target; const store = getPersistentStore(); if (!store) return null; try { const persisted = await store.lookup(normalizedKey); if (persisted?.version !== 1) return null; return params.readPersistedTarget ? params.readPersistedTarget(persisted.target) : persisted.target; } catch (error) { disablePersistentStore(error); return null; } }, delete(key) { const normalizedKey = key.trim(); if (!normalizedKey) return; memory.delete(normalizedKey); const store = getPersistentStore(); if (!store) return; store.delete(normalizedKey).catch(disablePersistentStore); }, clearForTest() { memory.clear(); persistentStore = void 0; persistentStoreDisabled = false; } }; } //#endregion export { APPROVAL_REACTION_BINDINGS, addApprovalReactionHintToText, approvalReactionDecisionSetsMatch, buildApprovalNativeControlsPromptText, buildApprovalPendingPromptPayload, buildApprovalReactionDeliveredBindingMarker, buildApprovalReactionHint, buildApprovalReactionPendingContent, buildApprovalReactionPendingContentForRequest, buildApprovalReactionPromptPayloadForRequest, createApprovalReactionTargetStore, hasApprovalReactionHintText, insertApprovalReactionHintNearIdHeader, listApprovalReactionBindings, normalizeApprovalReactionDecision, normalizeApprovalReactionEmoji, readApprovalReactionDecisionList, readApprovalReactionDeliveredBinding, readApprovalReactionDeliveryMetadata, readApprovalReactionPresentationBinding, resolveApprovalReactionDecision, resolveTypedApprovalReactionTarget, shouldSuppressLocalNativeExecApprovalPrompt };