UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

241 lines (240 loc) 9.01 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { m as FsSafeError } from "./path-BlG8lhgR.js"; import "./fs-safe-aqmM_n6V.js"; import { a as root } from "./secure-temp-dir-XAWcZnE2.js"; import { t as CONFIG_DIR } from "./utils-CCC-BEJH.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js"; import { r as logVerbose } from "./globals-GTrXU4s9.js"; import { n as assertSandboxPath } from "./sandbox-paths-DAj1Euvz.js"; import { s as getMediaDir, t as MEDIA_MAX_BYTES } from "./store-C9M_0A1p.js"; import { i as resolveInboundMediaReference } from "./media-reference-C05KIYAZ.js"; import { t as isInboundPathAllowed } from "./inbound-path-policy-CYWsER5a.js"; import { r as resolveChannelRemoteInboundAttachmentRoots } from "./channel-inbound-roots-D-Q8QssK.js"; import { T as slugifySessionKey } from "./docker-Z5I59fn0.js"; import { a as ensureSandboxWorkspaceForSession } from "./sandbox-BQ4p-0vf.js"; import { i as normalizeScpRemotePath, r as normalizeScpRemoteHost } from "./scp-host-D4uXCtRP.js"; import { fileURLToPath } from "node:url"; import path from "node:path"; import fs from "node:fs/promises"; import { spawn } from "node:child_process"; //#region src/auto-reply/reply/stage-sandbox-media.ts const STAGED_MEDIA_MAX_BYTES = MEDIA_MAX_BYTES; const SCP_STDERR_TAIL_CHARS = 16384; const EMPTY_STAGE_RESULT = { staged: /* @__PURE__ */ new Map() }; async function stageSandboxMedia(params) { const { ctx, sessionCtx, cfg, sessionKey, workspaceDir } = params; const hasPathsArray = Array.isArray(ctx.MediaPaths) && ctx.MediaPaths.length > 0; const rawPaths = resolveRawPaths(ctx); if (rawPaths.length === 0 || !sessionKey) return EMPTY_STAGE_RESULT; const sandbox = await ensureSandboxWorkspaceForSession({ config: cfg, sessionKey, workspaceDir }); const remoteMediaCacheDir = ctx.MediaRemoteHost ? path.join(CONFIG_DIR, "media", "remote-cache", slugifySessionKey(sessionKey)) : null; const effectiveWorkspaceDir = sandbox?.workspaceDir ?? remoteMediaCacheDir; 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(); for (const raw of rawPaths) { const source = resolveAbsolutePath(raw); if (!source || staged.has(source)) continue; if (!await isAllowedSourcePath({ source, mediaRemoteHost: ctx.MediaRemoteHost, remoteAttachmentRoots })) continue; const fileName = allocateStagedFileName(source, usedNames); if (!fileName) continue; const relativeDest = sandbox ? path.join("media", "inbound", fileName) : fileName; const dest = path.join(effectiveWorkspaceDir, relativeDest); try { if (ctx.MediaRemoteHost) await stageRemoteFileIntoRoot({ remoteHost: ctx.MediaRemoteHost, remotePath: source, rootDir: effectiveWorkspaceDir, relativeDestPath: relativeDest, maxBytes: STAGED_MEDIA_MAX_BYTES }); else await stageLocalFileIntoRoot({ sourcePath: await fs.realpath(source).catch(() => source), rootDir: effectiveWorkspaceDir, relativeDestPath: relativeDest, maxBytes: STAGED_MEDIA_MAX_BYTES }); } catch (err) { if (err instanceof FsSafeError && err.code === "too-large") logVerbose(`Blocking inbound media staging above ${STAGED_MEDIA_MAX_BYTES} bytes: ${source}`); else logVerbose(`Failed to stage inbound media path ${source}: ${String(err)}`); continue; } const stagedPath = sandbox ? path.posix.join("media", "inbound", fileName) : dest; staged.set(source, stagedPath); } rewriteStagedMediaPaths({ ctx, sessionCtx, rawPaths, staged, hasPathsArray }); return { staged }; } async function stageLocalFileIntoRoot(params) { await (await root(params.rootDir)).copyIn(params.relativeDestPath, params.sourcePath, { maxBytes: params.maxBytes }); } async function stageRemoteFileIntoRoot(params) { 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 scpFile(params.remoteHost, params.remotePath, tmpPath); await stageLocalFileIntoRoot({ sourcePath: tmpPath, rootDir: params.rootDir, relativeDestPath: params.relativeDestPath, maxBytes: params.maxBytes }); } finally { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } } function resolveRawPaths(ctx) { const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : void 0; return pathsFromArray && pathsFromArray.length > 0 ? pathsFromArray : normalizeOptionalString(ctx.MediaPath) ? [normalizeOptionalString(ctx.MediaPath)] : []; } function resolveAbsolutePath(value) { let resolved = value.trim(); if (!resolved) return null; if (resolved.startsWith("file://")) try { resolved = fileURLToPath(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 { await assertSandboxPath({ filePath: await fs.realpath(params.source).catch(() => params.source), 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 = path.basename(source); if (!baseName) return null; 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; } function rewriteStagedMediaPaths(params) { const rewriteIfStaged = (value) => { const raw = normalizeOptionalString(value); if (!raw) return value; const abs = resolveAbsolutePath(raw); if (!abs) return value; return params.staged.get(abs) ?? value; }; const nextMediaPaths = params.hasPathsArray ? params.rawPaths.map((p) => rewriteIfStaged(p) ?? p) : void 0; if (nextMediaPaths) { params.ctx.MediaPaths = nextMediaPaths; params.sessionCtx.MediaPaths = nextMediaPaths; params.ctx.MediaPath = nextMediaPaths[0]; params.sessionCtx.MediaPath = nextMediaPaths[0]; } else { const rewritten = rewriteIfStaged(params.ctx.MediaPath); if (rewritten && rewritten !== params.ctx.MediaPath) { params.ctx.MediaPath = rewritten; params.sessionCtx.MediaPath = rewritten; } } if (Array.isArray(params.ctx.MediaUrls) && params.ctx.MediaUrls.length > 0) { const nextUrls = params.ctx.MediaUrls.map((u) => rewriteIfStaged(u) ?? u); params.ctx.MediaUrls = nextUrls; params.sessionCtx.MediaUrls = nextUrls; } const rewrittenUrl = rewriteIfStaged(params.ctx.MediaUrl); if (rewrittenUrl && rewrittenUrl !== params.ctx.MediaUrl) { params.ctx.MediaUrl = rewrittenUrl; params.sessionCtx.MediaUrl = rewrittenUrl; } } async function scpFile(remoteHost, remotePath, localPath) { const safeRemoteHost = normalizeScpRemoteHost(remoteHost); if (!safeRemoteHost) throw new Error("invalid remote host for SCP"); const safeRemotePath = normalizeScpRemotePath(remotePath); if (!safeRemotePath) throw new Error("invalid remote path for SCP"); return new Promise((resolve, reject) => { const child = spawn("scp", [ "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes", "--", `${safeRemoteHost}:${safeRemotePath}`, localPath ], { stdio: [ "ignore", "ignore", "pipe" ] }); let stderr = ""; child.stderr?.setEncoding("utf8"); child.stderr?.on("data", (chunk) => { stderr = appendScpStderrTail(stderr, chunk); }); child.once("error", reject); child.once("exit", (code) => { if (code === 0) resolve(); else reject(/* @__PURE__ */ new Error(`scp failed (${code}): ${stderr.trim()}`)); }); }); } function appendScpStderrTail(current, chunk, maxChars = SCP_STDERR_TAIL_CHARS) { const combined = `${current}${chunk}`; if (combined.length <= maxChars) return combined; return combined.slice(-maxChars); } //#endregion export { stageSandboxMedia as t };