UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

617 lines (616 loc) • 23.2 kB
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs"; import { t as _usingCtx } from "./usingCtx-DwOBYOJf.mjs"; import { t as AgentSelectionRequiredError } from "./agent-scope-config-CDYpzA0r.mjs"; import { s as readSessionTranscriptUpdateVersion } from "./transcript-events-DUHAZLVw.mjs"; import { _ as validateArtifactsGetParams, g as validateArtifactsDownloadParams, v as validateArtifactsListParams } from "./src-l2dWvEsN.mjs"; import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-Dh56LniQ.mjs"; import { r as isSessionTranscriptProjectionUnavailableError } from "./session-transcript-projection-error-B7C6SQei.mjs"; import { r as resolveSessionTranscriptReadFence } from "./session-transcript-read-fence-DfxRHzep.mjs"; import "./session-accessor.sqlite-active-events-g60ynzeU.mjs"; import { a as MAX_PAYLOAD_BYTES } from "./server-constants-BH_EolwD.mjs"; import { a as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-Bxi11ET4.mjs"; import { i as resolveSessionStoreIdentity } from "./session-store-key-Ci099tm9.mjs"; import { n as readSessionArtifacts } from "./session-transcript-readers-B2XS9yB_.mjs"; import { a as readGatewayRequestMutationAuthority } from "./session-mutation-guards-Cw1VPolZ.mjs"; import { n as authorizeIncognitoSessionTarget } from "./session-sharing-policy-DBD0nzAz.mjs"; import { n as getSessionRowProjection } from "./session-row-projection-access-C2RDnj28.mjs"; import { l as createSessionListEntryFilter } from "./session-sharing-target-read-B1Z4GF6o.mjs"; import "./session-sharing-C16h_pTr.mjs"; import { r as prepareSessionMutationFacts, t as SessionMutationFactsUnavailableError } from "./session-sharing-preparation-phcGMAAc.mjs"; import { t as parseTranscriptImageArtifactId } from "./transcript-image-artifacts-DqNH9hLC.mjs"; import { t as resolveChatAttachmentFrameBudgetBytes } from "./chat-attachment-frame-budget-BBmKdOhk.mjs"; import { d as resolveManagedOutgoingMediaArtifactDownload, f as resolveManagedOutgoingMediaUrlDownload } from "./managed-image-attachments-qLUeEsQ8.mjs"; import { o as parseManagedOutgoingArtifactId } from "./managed-outgoing-artifact-id-Ba11Ib46.mjs"; import { t as assertValidParams } from "./validation-Bu27N0nv.mjs"; import { n as canCreateArtifactDownload, r as createArtifactDownload } from "./artifact-download-grants-tzwDxk5o.mjs"; import { i as toArtifactSummary } from "./artifacts-content-DvXNkzKV.mjs"; import { t as resolveSessionForRun } from "./server-session-key-DAk1WTzi.mjs"; import { randomUUID } from "node:crypto"; //#region src/gateway/server-methods/artifacts-session-resolution.ts /** RPCs and HTTP readers release their facts unless a download grant adopts them. */ function createArtifactSessionAccess(params) { const releases = /* @__PURE__ */ new Set(); return { ...params, retain(release) { releases.add(release); }, detach(release) { releases.delete(release); }, [Symbol.dispose]() { for (const release of releases) release(); releases.clear(); } }; } function resolveQuerySession(query, cfg, projection) { const selected = !query.sessionKey && query.runId ? resolveSessionForRun(query.runId, { ...query.agentId ? { agentId: query.agentId } : {}, ...projection ? { projection } : {} }) : void 0; const sessionKey = query.sessionKey ?? selected?.sessionKey; if (!sessionKey) return; let agentId = query.agentId ?? selected?.agentId; if (query.sessionKey) { const owner = resolveRequestedSessionAgentId(cfg, sessionKey, agentId); if (!owner.ok) throw new ArtifactSessionResolutionError(owner.error); agentId = owner.agentId; } const identity = resolveSessionStoreIdentity({ cfg, sessionKey, agentId, preserveQualifiedAddress: !query.sessionKey }); return { sessionKey: identity.canonicalKey, agentId: identity.agentId }; } var ArtifactSessionResolutionError = class extends Error { constructor(shape) { super(shape.message); this.shape = shape; } }; function throwArtifactSessionReadError(error) { if (error instanceof SessionMutationFactsUnavailableError) throw new ArtifactSessionResolutionError(errorShape(ErrorCodes.UNAVAILABLE, "session changed while reading artifact; reload the conversation", { retryable: true })); throw error; } function artifactResponseIsCurrent(found, respond) { try { found.assertCurrent?.(); return true; } catch (error) { if (!(error instanceof ArtifactSessionResolutionError)) throw error; respond(false, void 0, error.shape); return false; } } async function prepareArtifactSessionResolution(input, projection) { const query = { ...input }; if (!query.sessionKey && query.runId && projection?.sharingRevision === void 0) await projection?.ensureMaterialized(); return async (access) => { const { client } = access; const resolveSession = (cfg) => resolveQuerySession(query, cfg, projection); const cfg = access.getRuntimeConfig(); const resolved = resolveSession(cfg); if (!resolved) return; const facts = await prepareSessionMutationFacts({ cfg, sessionKey: resolved.sessionKey, agentId: resolved.agentId, preserveQualifiedAddress: !query.sessionKey, allowMissing: true }).catch(throwArtifactSessionReadError); access.retain(facts.release); const readCurrent = () => { try { const currentConfig = access.getRuntimeConfig(); const selected = resolveSession(currentConfig); if (selected?.sessionKey !== resolved.sessionKey || selected.agentId !== resolved.agentId) throw new SessionMutationFactsUnavailableError(); const current = facts.readCurrent(currentConfig); const { target } = current; const error = authorizeIncognitoSessionTarget({ client, sessionKey: query.sessionKey ?? resolved.sessionKey, target }); const visibilityDenied = Boolean(target && createSessionListEntryFilter({ client, cfg: currentConfig })?.(target.storeKey, target.entry) === false); if (!error && !visibilityDenied) return current; throw new ArtifactSessionResolutionError(query.sessionKey && error ? error : errorShape(ErrorCodes.INVALID_REQUEST, "no session found for artifact query", { details: { type: "artifact_scope_not_found" } })); } catch (error) { return throwArtifactSessionReadError(error); } }; return { sessionKey: facts.storageTarget.canonicalKey, agentId: facts.storageTarget.agentId, ...readCurrent(), readCurrent, release: facts.release }; }; } //#endregion //#region src/gateway/server-methods/artifacts-image-page.ts const CURSOR_TTL_MS = 9e5; const cursors = /* @__PURE__ */ new WeakMap(); const internalCaller = {}; async function readArtifactImagePage(params) { const owner = params.client ?? internalCaller; let state = cursors.get(owner); if (!state) { state = /* @__PURE__ */ new Map(); cursors.set(owner, state); } const now = Date.now(); for (const [key, value] of state) if (value.expiresAt <= now) state.delete(key); const cursor = params.cursor ? state.get(params.cursor) : void 0; if (params.cursor && (!cursor || cursor.binding !== params.binding)) throw new ArtifactSessionResolutionError(errorShape(ErrorCodes.INVALID_REQUEST, "Image cursor expired or belongs to another session; restart artifacts.list", { details: { type: "artifact_cursor_invalid" } })); const page = await readSessionArtifacts(params.scope, { kind: "image-page", sessionKey: params.sessionKey, ...params.filters, limit: params.limit, beforeSeq: cursor?.beforeSeq, imageOffset: cursor?.imageOffset, readWindow: cursor?.readWindow }).catch((error) => { if (cursor && isSessionTranscriptProjectionUnavailableError(error)) throw new ArtifactSessionResolutionError(errorShape(ErrorCodes.INVALID_REQUEST, "Transcript changed; restart artifacts.list", { details: { type: "artifact_cursor_invalid" } })); throw error; }); let nextCursor; if (page.next) { nextCursor = randomUUID(); state.set(nextCursor, { ...page.next, binding: params.binding, expiresAt: now + CURSOR_TTL_MS }); pruneMapToMaxSize(state, 128); } return { artifacts: page.artifacts, ...nextCursor ? { nextCursor } : {}, ...page.omittedOversized ? { omittedOversized: true } : {} }; } //#endregion //#region src/gateway/server-methods/artifacts-session-read.ts /** Authorization and transcript reads retain the same selected physical target. */ async function prepareArtifactSessionRead(query, access) { const selected = await (await prepareArtifactSessionResolution(query, access.projection))(access); if (!selected) return; const { sessionKey, release } = selected; const initial = selected.readCurrent(); const { target } = initial; const entry = target?.entry; const sessionId = entry?.sessionId; const storePath = initial.sourcePath ?? target?.storePath; const lifecycleRevision = entry?.lifecycleRevision; const assertCurrent = () => { const current = selected.readCurrent(); if ((current.sourcePath ?? current.target?.storePath) !== storePath || current.target?.entry.sessionId !== sessionId || current.target?.entry.lifecycleRevision !== lifecycleRevision) throw new ArtifactSessionResolutionError(errorShape(ErrorCodes.UNAVAILABLE, "session changed while reading artifact; reload the conversation", { retryable: true })); }; if (!sessionId || !storePath) return { sessionKey, assertCurrent, release }; return { sessionKey, scope: { agentId: initial.sourceAgentId ?? selected.agentId, sessionEntry: entry, sessionId, sessionKey, storePath }, assertCurrent, release }; } //#endregion //#region src/gateway/server-methods/artifacts-transcript-images.ts /** Recover only the referenced persisted bitmap; transcript bytes remain in their existing owner. */ async function findTranscriptImageArtifact(params, access, includeData) { const selected = await prepareArtifactSessionRead(params, access); if (!selected?.scope) return { sessionKey: selected?.sessionKey }; const { artifact } = await readSessionArtifacts(selected.scope, { kind: "image", sessionKey: selected.sessionKey, artifactId: params.artifactId, includeData, runId: params.runId, messageRole: params.messageRole }); selected.assertCurrent(); return { sessionKey: selected.sessionKey, assertCurrent: selected.assertCurrent, artifact }; } //#endregion //#region src/gateway/server-methods/artifacts.ts const queuedArtifactDownloads = /* @__PURE__ */ new Map(); function createArtifactRequestAccess(request) { const { assertCurrent } = readGatewayRequestMutationAuthority(request); const { context, respond } = request; assertCurrent(); return Object.assign(createArtifactSessionAccess({ client: request.client, projection: getSessionRowProjection(context), getRuntimeConfig: () => { assertCurrent(); return context.getRuntimeConfig(); } }), { assertCurrent, respond: (...args) => { assertCurrent(); respond(...args); } }); } function artifactError(type, message, details) { return errorShape(ErrorCodes.INVALID_REQUEST, message, { details: { type, ...details } }); } async function loadArtifacts(query, access, opts = {}) { const selected = await prepareArtifactSessionRead(query, access); if (!selected?.scope) return { sessionKey: selected?.sessionKey, artifacts: [] }; const { sessionKey, scope, assertCurrent } = selected; const { storePath, sessionId, sessionEntry: entry } = scope; if (query.type === "image") { const page = await readArtifactImagePage({ scope, binding: JSON.stringify([ sessionKey, scope.agentId, sessionId, query.runId, query.messageRole ]), client: access.client, cursor: query.cursor, limit: query.limit ?? 4, sessionKey, filters: { runId: query.runId, messageRole: query.messageRole } }); assertCurrent(); return { ...page, sessionKey, assertCurrent }; } const downloadIds = opts.downloadArtifactId ? /* @__PURE__ */ new Set([opts.downloadArtifactId]) : void 0; const queuedKey = downloadIds && !resolveSessionTranscriptReadFence(scope) ? JSON.stringify([ storePath, scope.agentId, sessionKey, sessionId, entry?.lifecycleRevision, query.runId, query.messageRole, readSessionTranscriptUpdateVersion() ]) : void 0; const queued = queuedKey ? queuedArtifactDownloads.get(queuedKey) : void 0; if (queued && opts.downloadArtifactId) { queued.ids.add(opts.downloadArtifactId); const loaded = await queued.result; assertCurrent(); return { ...loaded, assertCurrent }; } const collect = async () => { if (queuedKey) queuedArtifactDownloads.delete(queuedKey); const { artifacts } = await readSessionArtifacts(scope, { kind: "list", sessionKey, runId: query.runId, messageRole: query.messageRole, includeDownloadData: opts.includeDownloadData, downloadArtifactIds: downloadIds ? [...downloadIds] : void 0 }); return { sessionKey, artifacts }; }; const result = queuedKey && downloadIds ? Promise.resolve().then(collect) : collect(); if (queuedKey && downloadIds) queuedArtifactDownloads.set(queuedKey, { ids: downloadIds, result }); const loaded = await result; assertCurrent(); return { ...loaded, assertCurrent }; } function requireQueryable(params, respond) { if (params.sessionKey || params.runId) return true; respond(false, void 0, artifactError("artifact_query_unsupported", "artifacts require sessionKey or runId")); return false; } function respondArtifactNotFound(respond, requestedArtifactId) { respond(false, void 0, artifactError("artifact_not_found", "artifact not found", { artifactId: requestedArtifactId })); } async function runArtifactSessionOperation(respond, operation) { try { return { ok: true, value: await operation() }; } catch (error) { if (error instanceof ArtifactSessionResolutionError) { respond(false, void 0, error.shape); return { ok: false }; } if (error instanceof AgentSelectionRequiredError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message)); return { ok: false }; } throw error; } } async function findArtifact(params, access, opts = {}) { if (parseTranscriptImageArtifactId(params.artifactId)) return findTranscriptImageArtifact(params, access, opts.includeDownloadData !== false); const loaded = await loadArtifacts(params, access, opts); return { sessionKey: loaded.sessionKey, artifact: loaded.artifacts.find((artifact) => artifact.id === params.artifactId), assertCurrent: loaded.assertCurrent }; } async function prepareDownload(query, access) { const selected = await prepareArtifactSessionRead(query, access); if (!selected?.scope) return { sessionKey: selected?.sessionKey }; const { selection } = await readSessionArtifacts(selected.scope, { ...query, kind: "download-grant", sessionKey: selected.sessionKey }); selected.assertCurrent(); const result = { sessionKey: selected.sessionKey, assertCurrent: selected.assertCurrent, release: selected.release }; return selection?.kind === "prepared" ? { ...result, artifact: selection.download.artifact, download: selection.download } : { ...result, artifact: selection?.artifact }; } async function respondManagedArtifactDownload(query, access, respond, matched) { await runArtifactSessionOperation(respond, async () => { const resolved = await (await prepareArtifactSessionResolution(query, access.projection))(access); const cfg = access.getRuntimeConfig(); const defaultAgentId = resolved ? tryResolveSessionCompatibilityOwnerAgentId(cfg, resolved.sessionKey) : void 0; const managed = resolved && (!matched || matched.sessionKey === resolved.sessionKey) ? await resolveManagedOutgoingMediaArtifactDownload({ sessionKey: resolved.sessionKey, ...resolved.agentId ? { agentId: resolved.agentId } : {}, ...defaultAgentId ? { defaultAgentId } : {}, artifactId: query.artifactId }) : null; resolved?.readCurrent(); if (!managed) { respondArtifactNotFound(respond, query.artifactId); return; } respond(true, { artifact: { id: managed.artifactId, type: managed.type, title: managed.title, ...managed.mimeType ? { mimeType: managed.mimeType } : {}, ...managed.sizeBytes !== void 0 ? { sizeBytes: managed.sizeBytes } : {}, sessionKey: managed.sessionKey, ...matched?.runId ? { runId: matched.runId } : {}, ...matched?.messageSeq !== void 0 ? { messageSeq: matched.messageSeq } : {}, source: "session-transcript", download: { mode: "url" } }, url: managed.url, expiresAt: managed.expiresAt }); }); } const artifactsHandlers = { "artifacts.list": async (request) => { try { var _usingCtx$1 = _usingCtx(); const { params } = request; const access = _usingCtx$1.u(createArtifactRequestAccess(request)); const { respond } = access; if (!assertValidParams(params, validateArtifactsListParams, "artifacts.list", respond)) return; if (!requireQueryable(params, respond)) return; if (params.type !== "image" && (params.limit !== void 0 || params.cursor !== void 0)) { respond(false, void 0, artifactError("artifact_query_unsupported", "limit and cursor require type image")); return; } const query = { ...params }; const loaded = await runArtifactSessionOperation(respond, () => loadArtifacts(query, access, { includeDownloadData: false })); if (!loaded.ok) return; const { artifacts, sessionKey, nextCursor, omittedOversized } = loaded.value; if (!sessionKey && query.runId) { respond(false, void 0, artifactError("artifact_scope_not_found", "no session found for artifact query")); return; } if (artifactResponseIsCurrent(loaded.value, respond)) respond(true, { artifacts: artifacts.map(toArtifactSummary), ...nextCursor ? { nextCursor } : {}, ...omittedOversized ? { omittedOversized: true } : {} }); } catch (_) { _usingCtx$1.e = _; } finally { _usingCtx$1.d(); } }, "artifacts.get": async (request) => { try { var _usingCtx3 = _usingCtx(); const { params } = request; const access = _usingCtx3.u(createArtifactRequestAccess(request)); const { respond } = access; if (!assertValidParams(params, validateArtifactsGetParams, "artifacts.get", respond)) return; if (!requireQueryable(params, respond)) return; const query = { ...params }; const found = await runArtifactSessionOperation(respond, () => findArtifact(query, access, { includeDownloadData: false })); if (!found.ok) return; const { artifact } = found.value; if (!artifact) { respondArtifactNotFound(respond, query.artifactId); return; } if (artifactResponseIsCurrent(found.value, respond)) respond(true, { artifact: toArtifactSummary(artifact) }); } catch (_) { _usingCtx3.e = _; } finally { _usingCtx3.d(); } }, "artifacts.download": async (request) => { try { var _usingCtx4 = _usingCtx(); const { params, client } = request; const access = _usingCtx4.u(createArtifactRequestAccess(request)); const { respond, assertCurrent } = access; if (!assertValidParams(params, validateArtifactsDownloadParams, "artifacts.download", respond)) return; if (!requireQueryable(params, respond)) return; const query = { ...params }; if (query.sessionKey && !query.runId && !query.messageRole && parseManagedOutgoingArtifactId(query.artifactId)) { await respondManagedArtifactDownload(query, access, respond); return; } let found = await runArtifactSessionOperation(respond, () => query.transport === "http" && canCreateArtifactDownload(client) ? prepareDownload(query, access) : findArtifact(query, access, { downloadArtifactId: query.artifactId })); assertCurrent(); if (found.ok && found.value.download && !canCreateArtifactDownload(client)) { found = await runArtifactSessionOperation(respond, () => findArtifact(query, access, { downloadArtifactId: query.artifactId })); assertCurrent(); } if (!found.ok) return; const { artifact } = found.value; if (!artifact) { respondArtifactNotFound(respond, query.artifactId); return; } if (parseManagedOutgoingArtifactId(artifact.id)) { await respondManagedArtifactDownload(query, access, respond, artifact); return; } if (artifact.download.mode === "unsupported") { respond(false, void 0, artifactError("artifact_download_unsupported", "artifact download is unsupported", { artifactId: artifact.id })); return; } if (found.value.download) { const assertArtifactCurrent = found.value.assertCurrent; const download = createArtifactDownload({ client, prepared: found.value.download, release: found.value.release, assertCurrent: () => { assertCurrent(); assertArtifactCurrent?.(); }, read: async (response) => { try { var _usingCtx5 = _usingCtx(); const reader = _usingCtx5.u(createArtifactSessionAccess({ getRuntimeConfig: access.getRuntimeConfig, client, projection: access.projection })); const selected = await prepareArtifactSessionRead(query, reader); if (!selected?.scope) return; const current = await readSessionArtifacts(selected.scope, { ...query, kind: "download-response", sessionKey: selected.sessionKey, response }); selected.assertCurrent(); return current.response; } catch (_) { _usingCtx5.e = _; } finally { _usingCtx5.d(); } } }); access.detach(found.value.release); respond(true, { artifact: { ...toArtifactSummary(artifact), download: { mode: "url" } }, ...download }); return; } const managedUrl = artifact.download.mode === "url" && artifact.url && artifact.sessionKey ? await resolveManagedOutgoingMediaUrlDownload({ sessionKey: artifact.sessionKey, url: artifact.url }) : null; if (!artifactResponseIsCurrent(found.value, respond)) return; const payload = { artifact: toArtifactSummary(artifact), ...artifact.download.mode === "bytes" ? { encoding: "base64", data: artifact.data } : {}, ...artifact.download.mode === "url" ? { url: managedUrl?.url ?? artifact.url, ...managedUrl ? { expiresAt: managedUrl.expiresAt } : {} } : {} }; if (artifact.download.mode === "bytes") { const maxBase64Length = 4 * Math.floor(resolveChatAttachmentFrameBudgetBytes(MAX_PAYLOAD_BYTES) / 3); const envelopeBytes = Buffer.byteLength(JSON.stringify({ type: "res", id: request.req.id, ok: true, payload: { ...payload, data: "" } })); if ((artifact.data?.length ?? 0) > Math.min(maxBase64Length, 26214400 - envelopeBytes)) { respond(false, void 0, artifactError("artifact_download_unsupported", "artifact is too large for inline transfer; request transport: \"http\"", { artifactId: artifact.id })); return; } } respond(true, payload); } catch (_) { _usingCtx4.e = _; } finally { _usingCtx4.d(); } } }; //#endregion export { artifactsHandlers };