openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
263 lines (262 loc) • 10.2 kB
JavaScript
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 };