UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

263 lines (262 loc) 10.2 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { n as sliceUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { h as readLocalFileSafely, t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js"; import { t as CONFIG_DIR } from "./utils-P__uGsPB.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js"; import { t as retryAsync } from "./retry-DIUON3ys.js"; import { r as logVerbose } from "./globals-CTaGxEqj.js"; import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js"; import { s as getMediaDir } from "./store-Ph57PZ9P.js"; import { l as normalizeMediaFacts } from "./media-facts-8Sl_nPCc.js"; import { a as safeFileURLToPath } from "./local-file-access-B1HKoQAc.js"; import { n as assertSandboxPath } from "./sandbox-paths-hdOwYXJD.js"; import { o as resolveInboundMediaReference } from "./media-reference-ZG0ZXjCX.js"; import { t as isInboundPathAllowed } from "./inbound-path-policy-DQ5Rksw7.js"; import { i as slugifySessionKey } from "./shared-DdMEwpA0.js"; import { t as ensureSandboxWorkspaceForSession } from "./context-B_khSODz.js"; import "./sandbox-A2KOznlu.js"; import { r as resolveChannelRemoteInboundAttachmentRoots } from "./channel-inbound-roots-BiNZ8OaF.js"; import { c as stagedInputDirectory, i as ensureStagedInputDirectory, l as stagedInputFileName } from "./staged-inputs-CJwnNq26.js"; import { i as normalizeScpRemotePath, r as normalizeScpRemoteHost } from "./scp-host-3FoHUz1i.js"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; //#region src/auto-reply/reply/stage-sandbox-media.ts /** Maximum size of one file copied into an agent sandbox or staging workspace. */ const SANDBOX_MEDIA_MAX_BYTES = 52428800; const SCP_STDERR_TAIL_CHARS = 16384; const EMPTY_STAGE_RESULT = { staged: /* @__PURE__ */ new Map() }; async function stageSandboxMedia(params) { const { ctx, sessionCtx, cfg, sessionKey, workspaceDir, abortSignal } = params; abortSignal?.throwIfAborted(); const media = normalizeMediaFacts(ctx.media); const pathEntries = media.flatMap((fact, index) => { const mediaPath = normalizeOptionalString(fact.path); return mediaPath ? [{ index, path: mediaPath }] : []; }); if (pathEntries.length === 0 || !sessionKey) return EMPTY_STAGE_RESULT; const sandbox = ctx.MediaRemoteHost && params.remoteMediaMode === "cache" ? null : await ensureSandboxWorkspaceForSession({ config: cfg, agentId: params.agentId, sessionKey, workspaceDir }); const remoteMediaCacheDir = ctx.MediaRemoteHost ? path.join(CONFIG_DIR, "media", "remote-cache", slugifySessionKey(sessionKey)) : null; const effectiveWorkspaceDir = sandbox?.workspaceDir ?? remoteMediaCacheDir ?? workspaceDir; if (!effectiveWorkspaceDir) return EMPTY_STAGE_RESULT; await fs.mkdir(effectiveWorkspaceDir, { recursive: true }); const remoteAttachmentRoots = ctx.MediaRemoteHost ? resolveChannelRemoteInboundAttachmentRoots({ cfg, ctx }) ?? [] : []; const usedNames = /* @__PURE__ */ new Set(); const staged = /* @__PURE__ */ new Map(); const stagedUrlAliases = /* @__PURE__ */ new Set(); const inputDirectory = stagedInputDirectory(crypto.randomUUID()); let stagingReady = false; for (const entry of pathEntries) { abortSignal?.throwIfAborted(); const source = await resolveStageableMediaSource(entry.path); if (!source) continue; if (!await isAllowedSourcePath({ source, mediaRemoteHost: ctx.MediaRemoteHost, remoteAttachmentRoots })) continue; const fileName = allocateStagedFileName(source, usedNames); const relativeDest = path.posix.join(inputDirectory, fileName); const dest = path.join(effectiveWorkspaceDir, relativeDest); try { if (!stagingReady) { await ensureStagedInputDirectory(effectiveWorkspaceDir, inputDirectory, abortSignal); stagingReady = true; } if (ctx.MediaRemoteHost) await stageRemoteFileIntoRoot({ remoteHost: ctx.MediaRemoteHost, remotePath: source, rootDir: effectiveWorkspaceDir, relativeDestPath: relativeDest, abortSignal }); else await stageLocalFileIntoRoot({ sourcePath: await fs.realpath(source).catch(() => source), rootDir: effectiveWorkspaceDir, relativeDestPath: relativeDest, abortSignal }); } catch (err) { if (abortSignal?.aborted && Object.is(err, abortSignal.reason)) throw err; if (err instanceof FsSafeError && err.code === "too-large") console.warn(`Inbound media staging skipped for ${fileName}: ${err.message}`); else logVerbose(`Failed to stage inbound media path ${source}: ${String(err)}`); continue; } const stagedPath = sandbox ? relativeDest : dest; staged.set(entry.index, stagedPath); if (await isUrlAliasForStagedSource({ url: media[entry.index]?.url, sourcePath: entry.path, source, mediaRemoteHost: ctx.MediaRemoteHost })) stagedUrlAliases.add(entry.index); } abortSignal?.throwIfAborted(); if (staged.size === 0) return { staged }; const nextMedia = [...media]; for (const [index, stagedPath] of staged) { const fact = nextMedia[index]; if (fact) nextMedia[index] = { ...fact, path: stagedPath, ...stagedUrlAliases.has(index) ? { url: stagedPath } : {}, workspaceDir: effectiveWorkspaceDir, staged: true }; } ctx.media = nextMedia; if (sessionCtx !== ctx) sessionCtx.media = nextMedia; return { staged }; } async function isUrlAliasForStagedSource(params) { const url = normalizeOptionalString(params.url); if (!url) return false; if (url === params.sourcePath) return true; const sourceAbsolutePath = resolveAbsolutePath(params.sourcePath); const urlAbsolutePath = resolveAbsolutePath(url); if (sourceAbsolutePath && urlAbsolutePath && path.normalize(sourceAbsolutePath) === path.normalize(urlAbsolutePath)) return true; if (params.mediaRemoteHost) return false; const urlSource = await resolveStageableMediaSource(url); if (!urlSource) return false; const [sourceIdentity, urlIdentity] = await Promise.all([resolveLocalSourceIdentity(params.source), resolveLocalSourceIdentity(urlSource)]); return sourceIdentity === urlIdentity; } async function resolveLocalSourceIdentity(sourcePath) { return await fs.realpath(sourcePath).catch(() => path.resolve(sourcePath)); } async function resolveStageableMediaSource(value) { const raw = value.trim(); if (!raw) return null; return (await resolveInboundMediaReference(raw).catch(() => null))?.physicalPath ?? resolveAbsolutePath(raw); } async function stageLocalFileIntoRoot(params) { const root$1 = await root(params.rootDir); const source = await readLocalFileSafely({ filePath: params.sourcePath, maxBytes: SANDBOX_MEDIA_MAX_BYTES }); params.abortSignal?.throwIfAborted(); await root$1.create(params.relativeDestPath, source.buffer); } async function stageRemoteFileIntoRoot(params) { const { abortSignal } = params; const safeRemoteHost = normalizeScpRemoteHost(params.remoteHost); if (!safeRemoteHost) throw new Error("invalid remote host for SCP"); const safeRemotePath = normalizeScpRemotePath(params.remotePath); if (!safeRemotePath) throw new Error("invalid remote path for SCP"); const tmpRoot = resolvePreferredOpenClawTmpDir(); await fs.mkdir(tmpRoot, { recursive: true }); const tmpDir = await fs.mkdtemp(path.join(tmpRoot, "stage-sandbox-media-")); const tmpPath = path.join(tmpDir, "download"); try { await retryAsync(async () => { if (abortSignal?.aborted) return; const result = await runCommandWithTimeout([ "scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes", "--", `${safeRemoteHost}:${safeRemotePath}`, tmpPath ], { signal: abortSignal, killProcessTree: true, maxOutputBytes: { stdout: 1, stderr: SCP_STDERR_TAIL_CHARS * 4 } }); if (result.code !== 0) { if (result.code === null && result.termination === "signal" && abortSignal?.aborted) return; const stderr = sliceUtf16Safe(result.stderr, -16384).trim(); throw new Error(`scp failed (${result.code}): ${stderr}`); } }, { attempts: 3, label: "remote inbound media SCP", shouldRetry: () => !abortSignal?.aborted }); abortSignal?.throwIfAborted(); await stageLocalFileIntoRoot({ ...params, sourcePath: tmpPath }); } finally { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } } function resolveAbsolutePath(value) { let resolved = value.trim(); if (!resolved) return null; if (/^file:/iu.test(resolved)) try { resolved = safeFileURLToPath(resolved); } catch { return null; } if (!path.isAbsolute(resolved)) return null; return resolved; } async function isAllowedSourcePath(params) { if (params.mediaRemoteHost) { if (!isInboundPathAllowed({ filePath: params.source, roots: params.remoteAttachmentRoots })) { logVerbose(`Blocking remote media staging from disallowed attachment path: ${params.source}`); return false; } return true; } if (await resolveInboundMediaReference(params.source).catch(() => null)) return true; const mediaDir = getMediaDir(); const canonicalMediaDir = await fs.realpath(mediaDir).catch(() => mediaDir); if (!isInboundPathAllowed({ filePath: params.source, roots: [mediaDir, canonicalMediaDir] })) { logVerbose(`Blocking attempt to stage media from outside media directory: ${params.source}`); return false; } try { const canonicalSource = await fs.realpath(params.source).catch(() => params.source); await assertSandboxPath({ filePath: canonicalSource, cwd: canonicalMediaDir, root: canonicalMediaDir }); return true; } catch { logVerbose(`Blocking attempt to stage media from outside media directory: ${params.source}`); return false; } } function allocateStagedFileName(source, usedNames) { const baseName = stagedInputFileName(path.basename(source)); const parsed = path.parse(baseName); let fileName = baseName; let suffix = 1; while (usedNames.has(fileName)) { fileName = `${parsed.name}-${suffix}${parsed.ext}`; suffix += 1; } usedNames.add(fileName); return fileName; } //#endregion export { stageSandboxMedia as n, SANDBOX_MEDIA_MAX_BYTES as t };