UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

576 lines (575 loc) 20.7 kB
import { o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { c as normalizeOptionalLowercaseString, h as readNonEmptyStringPreservingWhitespace } from "./string-coerce-CIXf7egm.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { p as redactSensitiveText } from "./redact-BtvPPfTi.js"; import { w as resolveStateDir } from "./paths-D2sRr1a_.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { o as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-DYCYxE4w.js"; import { _ as isOpenClawStateWriteContentionError, c as runWithOpenClawStateBusyTimeout } from "./openclaw-state-db-BRTnL-D8.js"; import { t as isAllowedToolCallName } from "./tool-call-shared-BxbmRH0F.js"; import { i as buildAgentRunTerminalOutcomeFromLifecycleEvent, o as classifyAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-CigeY75d.js"; import { t as mergeAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-merge-DXNYLPhQ.js"; import { i as recordExecutionDecisionFact, r as pruneExpiredExecutionDecisionFacts } from "./execution-decision-facts-CuS1RntZ.js"; import { n as parseExecutionDecisionWork, r as processExecutionDecisionWork } from "./execution-decision-work-BR8wVcpq.js"; import { d as isOutboundMessageProgressInput, l as pruneExpiredAuditEvents, n as processExecutionIdentityAdmissionWork, o as pruneExpiredOutboundMessageProgress, r as pruneExpiredExecutionIdentityContexts, s as recordOutboundMessageProgress, u as recordAuditEvent } from "./execution-identity-context-B0gwJgCx.js"; import { createHash, randomUUID } from "node:crypto"; //#region src/audit/audit-event-writer.ts /** Non-blocking process-owned queue for audit metadata persistence. */ const MAX_PENDING_AUDIT_EVENTS = 4096; const AUDIT_MAINTENANCE_INTERVAL_MS = 36e5; const AUDIT_LOCK_RETRY_DELAY_MS = 25; const AUDIT_LOCK_RETRY_MAX_DELAY_MS = 1e3; const AUDIT_LOCK_CONTENTION_REPORT_MS = 1e3; const AUDIT_WRITER_SHUTDOWN_TIMEOUT_MS = OPENCLAW_SQLITE_BUSY_TIMEOUT_MS + 5e3; function formatAuditWriterError(error) { return truncateUtf16Safe(redactSensitiveText(error instanceof Error ? error.message : String(error), { mode: "tools" }), 512); } function executionIdentityFailureMessage(error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("audit identity key is missing") || message.includes("audit identity key is corrupt")) return "audit execution identity key unavailable"; if (message.includes("execution identity context conflict")) return "audit execution identity context conflict"; if (message.includes("execution identity recovery evidence unavailable")) return "audit execution identity recovery evidence unavailable"; if (message.includes("admission envelope") || message.includes("admission work") || message.includes("admission token")) return "audit execution identity envelope rejected"; return "audit execution identity persistence failed"; } /** Start one bounded queue; retain the owner environment or claimed state rejects its writes. */ function createAuditEventWriter(options = {}) { const database = { env: { ...process.env, OPENCLAW_STATE_DIR: options.stateDir ?? resolveStateDir(process.env) } }; const maxPending = Math.max(1, Math.floor(options.maxPending ?? MAX_PENDING_AUDIT_EVENTS)); const queue = []; let stopped = false; let unavailable = false; let maintenancePending = true; let readyPending = true; let scheduled; let retryTimer; let lockRetryAttempt = 0; let lockContentionDelayMs = 0; let lockContentionReported = false; let resolveReady; const ready = new Promise((resolve) => { resolveReady = resolve; }); let stopPromise; let resolveStop; let stopTimer; const fail = (error) => { options.onError?.(formatAuditWriterError(error)); }; const reportContention = (message) => { options.onContention?.(formatAuditWriterError(message)); }; const runWithoutBusyWait = (operation) => runWithOpenClawStateBusyTimeout(() => operation(), database, 0); const observeLockContention = () => { lockRetryAttempt += 1; }; const resetLockContention = () => { lockRetryAttempt = 0; lockContentionDelayMs = 0; lockContentionReported = false; }; const reportMaintenance = () => { let more = false; for (const maintenance of [ () => pruneExpiredAuditEvents({ database }), () => pruneExpiredExecutionIdentityContexts({ database }), () => pruneExpiredExecutionDecisionFacts({ database }), () => pruneExpiredOutboundMessageProgress({ database }) ]) try { more = runWithoutBusyWait(maintenance) > 0 || more; } catch (error) { if (isOpenClawStateWriteContentionError(error)) { observeLockContention(); return "retry"; } fail(error); } return more ? "more" : "settled"; }; const processRequest = (request) => { try { runWithoutBusyWait(() => { if (request.type === "record-event") { if (isOutboundMessageProgressInput(request.input)) recordOutboundMessageProgress(request.input, database); else recordAuditEvent(request.input, database); return; } if (request.type === "record-execution-identity") { processExecutionIdentityAdmissionWork(request.work, database); return; } if (request.type === "record-execution-decision-work") { processExecutionDecisionWork(request.work, database); return; } recordExecutionDecisionFact(request.receipt, database); }); return "settled"; } catch (error) { if (isOpenClawStateWriteContentionError(error)) { observeLockContention(); return "retry"; } resetLockContention(); if (request.type === "record-execution-identity") fail(executionIdentityFailureMessage(error)); else if (request.type === "record-execution-decision" || request.type === "record-execution-decision-work") fail("audit execution decision rejected"); else fail(error); return "settled"; } }; const finishStop = () => { if (stopTimer) { clearTimeout(stopTimer); stopTimer = void 0; } const finish = resolveStop; resolveStop = void 0; finish?.(); }; const schedule = () => { if (retryTimer) { if (stopped) retryTimer.ref?.(); return; } if (scheduled) { if (stopped) scheduled.ref?.(); return; } scheduled = setImmediate(drainOne); if (!stopped) scheduled.unref?.(); }; const scheduleRetry = () => { const delayMs = Math.min(AUDIT_LOCK_RETRY_MAX_DELAY_MS, AUDIT_LOCK_RETRY_DELAY_MS * 2 ** Math.min(6, Math.max(0, lockRetryAttempt - 1))); lockContentionDelayMs += delayMs; if (!lockContentionReported && lockContentionDelayMs >= AUDIT_LOCK_CONTENTION_REPORT_MS) { lockContentionReported = true; reportContention("audit event persistence delayed by SQLite lock contention"); } retryTimer = setTimeout(() => { retryTimer = void 0; drainOne(); }, delayMs); if (!stopped) retryTimer.unref?.(); }; function drainOne() { scheduled = void 0; if (maintenancePending) { const maintenance = reportMaintenance(); if (readyPending) { readyPending = false; resolveReady(); } if (maintenance === "retry") { scheduleRetry(); return; } maintenancePending = maintenance === "more"; resetLockContention(); } const request = queue.shift(); if (request && processRequest(request) === "retry") { queue.unshift(request); scheduleRetry(); return; } if (request) resetLockContention(); if (queue.length > 0 || maintenancePending) { schedule(); return; } if (stopped) finishStop(); } const maintenanceTimer = setInterval(() => { maintenancePending = true; schedule(); }, AUDIT_MAINTENANCE_INTERVAL_MS); maintenanceTimer.unref?.(); schedule(); const enqueue = (message) => { if (stopped || unavailable || queue.length >= maxPending) { if (!stopped) fail(unavailable ? "audit event writer is unavailable; dropping metadata" : `audit event queue is full (${maxPending}); dropping metadata`); return false; } try { const boundedMessage = message.type === "record-execution-decision-work" ? { ...message, work: parseExecutionDecisionWork(message.work) } : message; queue.push(structuredClone(boundedMessage)); schedule(); return true; } catch (error) { if (message.type !== "record-event") fail(message.type === "record-execution-identity" ? "audit execution identity envelope could not be queued" : "audit execution decision receipt could not be queued"); else { unavailable = true; fail(error); } return false; } }; return { ready, record: (input) => enqueue({ type: "record-event", input }), recordExecutionIdentity: (work) => enqueue({ type: "record-execution-identity", work }), recordExecutionDecision: (receipt) => enqueue({ type: "record-execution-decision", receipt }), recordExecutionDecisionWork: (work) => enqueue({ type: "record-execution-decision-work", work }), stop: () => { if (stopPromise) return stopPromise; stopped = true; clearInterval(maintenanceTimer); maintenancePending = true; stopPromise = new Promise((resolve) => { resolveStop = resolve; stopTimer = setTimeout(() => { queue.length = 0; if (scheduled) { clearImmediate(scheduled); scheduled = void 0; } if (retryTimer) { clearTimeout(retryTimer); retryTimer = void 0; } fail("audit event writer shutdown timed out; pending metadata may be lost"); finishStop(); }, AUDIT_WRITER_SHUTDOWN_TIMEOUT_MS); stopTimer.unref?.(); schedule(); }); return stopPromise; } }; } //#endregion //#region src/audit/agent-event-audit.ts /** Redaction-safe projection from live agent events into durable audit metadata. */ const MAX_TRACKED_RUN_INSTANCES = 1024; const log$1 = createSubsystemLogger("audit/events"); let persistenceFailureWarned$1 = false; function auditToolName(value) { const toolName = readNonEmptyStringPreservingWhitespace(value)?.trim(); if (!toolName) return; return isAllowedToolCallName(toolName, null) ? toolName : "unknown"; } function auditToolCallId(value) { const toolCallId = readNonEmptyStringPreservingWhitespace(value); if (!toolCallId) return; return `sha256:${createHash("sha256").update(toolCallId).digest("hex")}`; } function legacyAuditSourceId(params) { return `${params.runId}:${params.sourceSequence}:${params.occurredAt}:${params.action}`; } function projectExplicitAttribution(event) { const eventAgentId = readNonEmptyStringPreservingWhitespace(event.agentId); return { actorType: eventAgentId ? "agent" : "system", agentId: eventAgentId ?? "unknown", sessionKey: readNonEmptyStringPreservingWhitespace(event.sessionKey), sessionId: readNonEmptyStringPreservingWhitespace(event.sessionId) }; } const AUDIT_TERMINAL_BY_CLASSIFICATION = { success: { status: "succeeded" }, timeout: { status: "timed_out", errorCode: "run_timed_out" }, cancellation: { status: "cancelled", errorCode: "run_cancelled" }, failure: { status: "failed", errorCode: "run_failed" } }; function classifyRunTerminal(data, phase) { const outcome = buildAgentRunTerminalOutcomeFromLifecycleEvent({ phase, data }); if (outcome.reason === "blocked") return { outcome, status: "blocked", errorCode: "run_blocked" }; return { outcome, ...AUDIT_TERMINAL_BY_CLASSIFICATION[classifyAgentRunTerminalOutcome(outcome)] }; } function projectAgentEvent(event) { const runId = readNonEmptyStringPreservingWhitespace(event.runId); const phase = readNonEmptyStringPreservingWhitespace(event.data.phase); if (!runId || !phase) return; const provenance = projectExplicitAttribution(event); if (event.stream === "lifecycle" && phase === "start") { const occurredAt = asDateTimestampMs(event.data.startedAt) ?? event.ts; const action = "agent.run.started"; return { input: { sourceId: legacyAuditSourceId({ runId, sourceSequence: event.seq, occurredAt, action }), sourceSequence: event.seq, occurredAt, kind: "agent_run", action, status: "started", actorType: provenance.actorType, actorId: provenance.agentId, agentId: provenance.agentId, ...provenance.sessionKey ? { sessionKey: provenance.sessionKey } : {}, ...provenance.sessionId ? { sessionId: provenance.sessionId } : {}, runId } }; } if (event.stream === "lifecycle" && (phase === "end" || phase === "error")) { const { outcome, ...terminal } = classifyRunTerminal(event.data, phase); const occurredAt = asDateTimestampMs(event.data.endedAt) ?? event.ts; const action = "agent.run.finished"; return { input: { sourceId: legacyAuditSourceId({ runId, sourceSequence: event.seq, occurredAt, action }), sourceSequence: event.seq, occurredAt, kind: "agent_run", action, ...terminal, actorType: provenance.actorType, actorId: provenance.agentId, agentId: provenance.agentId, ...provenance.sessionKey ? { sessionKey: provenance.sessionKey } : {}, ...provenance.sessionId ? { sessionId: provenance.sessionId } : {}, runId }, terminal: { outcome, phase } }; } } /** Project the complete trusted tool-execution lifecycle without private diagnostic content. */ function projectToolExecutionEventToAudit(event) { if (event.type === "tool.execution.blocked" && event.deniedReason === "unsupported_tool_schema" && !readNonEmptyStringPreservingWhitespace(event.toolCallId)) return; const runId = readNonEmptyStringPreservingWhitespace(event.runId); const toolName = auditToolName(event.toolName); if (!runId || !toolName) return; const toolCallId = auditToolCallId(event.toolCallId); const provenance = projectExplicitAttribution(event); const occurredAt = asDateTimestampMs(event.sourceTimestampMs) ?? event.ts; const attribution = { sourceSequence: event.seq, occurredAt, kind: "tool_action", actorType: provenance.actorType, actorId: provenance.agentId, agentId: provenance.agentId, ...provenance.sessionKey ? { sessionKey: provenance.sessionKey } : {}, ...provenance.sessionId ? { sessionId: provenance.sessionId } : {}, runId, ...toolCallId ? { toolCallId } : {}, toolName }; if (event.type === "tool.execution.started") { const action = "tool.action.started"; return { sourceId: legacyAuditSourceId({ runId, sourceSequence: event.seq, occurredAt, action }), ...attribution, action, status: "started" }; } const errorCategory = event.type === "tool.execution.error" ? normalizeOptionalLowercaseString(event.errorCategory) : void 0; const terminalReason = event.type === "tool.execution.error" ? event.terminalReason : void 0; const diagnosticErrorCode = event.type === "tool.execution.error" ? normalizeOptionalLowercaseString(event.errorCode) : void 0; const toolCancelled = terminalReason === "cancelled" || terminalReason === void 0 && (errorCategory === "aborted" || errorCategory === "aborterror" || errorCategory === "cancelled" || errorCategory === "canceled"); const toolTimedOut = terminalReason === "timed_out"; const terminal = event.type === "tool.execution.completed" ? { status: "succeeded" } : event.type === "tool.execution.blocked" ? { status: "blocked", errorCode: "tool_blocked" } : diagnosticErrorCode === "tool_outcome_unknown" ? { status: "unknown", errorCode: "tool_outcome_unknown" } : toolCancelled ? { status: "cancelled", errorCode: "tool_cancelled" } : toolTimedOut ? { status: "timed_out", errorCode: "tool_timed_out" } : { status: "failed", errorCode: "tool_failed" }; const action = "tool.action.finished"; return { sourceId: legacyAuditSourceId({ runId, sourceSequence: event.seq, occurredAt, action }), ...attribution, action, ...terminal }; } /** Create the Gateway-owned non-blocking audit projection and persistence handle. */ function createAgentEventAuditRecorder(options) { const writer = options?.writer ?? createAuditEventWriter({ ...options?.stateDir ? { stateDir: options.stateDir } : {}, onContention: (message) => log$1.warn(message), onError: (error) => { if (!persistenceFailureWarned$1) { persistenceFailureWarned$1 = true; log$1.warn(`audit event persistence failed: ${error}`); } } }); const terminalSettleMs = Math.max(0, Math.floor(options?.terminalSettleMs ?? 15e3)); const pendingTerminals = /* @__PURE__ */ new Map(); const openRunInstances = /* @__PURE__ */ new Set(); const settledRunInstances = /* @__PURE__ */ new Set(); const rememberSettled = (runInstance) => { settledRunInstances.delete(runInstance); settledRunInstances.add(runInstance); if (settledRunInstances.size > MAX_TRACKED_RUN_INSTANCES) { const oldest = settledRunInstances.values().next().value; if (oldest !== void 0) settledRunInstances.delete(oldest); } }; const clearPending = (runInstance) => { const pending = pendingTerminals.get(runInstance); if (!pending) return; clearTimeout(pending.timer); pendingTerminals.delete(runInstance); }; const flushPending = (runInstance) => { const pending = pendingTerminals.get(runInstance); if (!pending) return; clearPending(runInstance); openRunInstances.delete(runInstance); if (writer.record(pending.input)) rememberSettled(runInstance); }; const scheduleTerminal = (runInstance, incoming) => { const existing = pendingTerminals.get(runInstance); let selected = incoming; if (existing) { if (existing.phase === "error" && incoming.phase === "end" && incoming.outcome.reason === "completed") selected = existing; else selected = mergeAgentRunTerminalOutcome(existing.outcome, incoming.outcome) === existing.outcome ? existing : incoming; clearTimeout(existing.timer); } const timer = setTimeout(() => flushPending(runInstance), terminalSettleMs); timer.unref?.(); pendingTerminals.delete(runInstance); pendingTerminals.set(runInstance, { ...selected, timer }); if (pendingTerminals.size > MAX_TRACKED_RUN_INSTANCES) { const oldest = pendingTerminals.keys().next().value; if (oldest !== void 0) flushPending(oldest); } }; return { record: (event) => { const projection = projectAgentEvent(event); if (!projection) return; const runInstance = `${event.lifecycleGeneration ?? "unknown"}\0${event.runId}`; if (!projection.terminal) { const alreadyOpen = openRunInstances.has(runInstance); clearPending(runInstance); settledRunInstances.delete(runInstance); if (alreadyOpen) return; openRunInstances.add(runInstance); writer.record(projection.input); return; } if (settledRunInstances.has(runInstance)) return; if (projection.terminal.outcome.reason === "completed" && !pendingTerminals.has(runInstance)) { openRunInstances.delete(runInstance); if (writer.record(projection.input)) rememberSettled(runInstance); return; } scheduleTerminal(runInstance, { input: projection.input, ...projection.terminal }); }, recordTool: (event) => { const input = projectToolExecutionEventToAudit(event); if (input) writer.record(input); }, stop: async () => { for (const runInstance of pendingTerminals.keys()) flushPending(runInstance); await writer.stop(); } }; } //#endregion //#region src/audit/audit-recorder.ts /** Gateway-owned recorder joining trusted run, tool, and message lifecycle streams. */ const log = createSubsystemLogger("audit/events"); let persistenceFailureWarned = false; function createAuditEventRecorder(options) { let nextAcceptedMessageSequence = 0; const writer = options.writer ?? createAuditEventWriter({ ...options.stateDir ? { stateDir: options.stateDir } : {}, onContention: (message) => log.warn(message), onError: (error) => { if (!persistenceFailureWarned) { persistenceFailureWarned = true; log.warn(`audit event persistence failed: ${error}`); } } }); return { ...createAgentEventAuditRecorder({ writer, ...options.terminalSettleMs !== void 0 ? { terminalSettleMs: options.terminalSettleMs } : {} }), recordExecutionIdentity: writer.recordExecutionIdentity, recordExecutionDecision: writer.recordExecutionDecision, recordExecutionDecisionWork: writer.recordExecutionDecisionWork, recordMessage: (event) => { if (options.messageMode === "off") return; if (options.messageMode === "direct" && event.conversationKind !== "direct") return; nextAcceptedMessageSequence += 1; writer.record({ ...event, sourceId: event.sourceId?.trim() || `message:${randomUUID()}`, sourceSequence: nextAcceptedMessageSequence }); } }; } //#endregion export { createAuditEventRecorder as t };