openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
200 lines (199 loc) • 7.85 kB
JavaScript
import "./fs-safe-defaults-B7hUN42l.js";
import { i as isPathInside } from "./path-BlG8lhgR.js";
import { S as safeFileURLToPath, b as hasEncodedFileUrlSeparator, v as assertNoWindowsNetworkPath } from "./fs-safe-aqmM_n6V.js";
import { c as assertNoPathAliasEscape } from "./secure-temp-dir-XAWcZnE2.js";
import { d as resolveConfigDir } from "./utils-CCC-BEJH.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js";
import "./path-guards-CBe_wA_B.js";
import { h as isWindowsDrivePath } from "./archive-Dcpo6Wva.js";
import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BL9SUd7E.js";
import "./local-file-access-CBe_wA_B.js";
import { URL } from "node:url";
import path from "node:path";
import os from "node:os";
//#region src/agents/sandbox-paths.ts
/**
* Sandbox input path normalization and boundary checks.
*
* Handles host paths, file URLs, temporary media paths, and workspace root assertions.
*/
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
const DATA_URL_RE = /^data:/i;
const SANDBOX_CONTAINER_WORKDIR = "/workspace";
const MANAGED_MEDIA_SUBDIRS = new Set(["outbound"]);
function normalizeUnicodeSpaces(str) {
return str.replace(UNICODE_SPACES, " ");
}
function normalizeAtPrefix(filePath) {
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
}
function expandPath(filePath) {
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
if (normalized === "~") return os.homedir();
if (normalized.startsWith("~/")) return os.homedir() + normalized.slice(1);
return normalized;
}
/** True when the path is absolute for the current platform or a Windows drive path (e.g. C:\\...), even if path.isAbsolute is false under POSIX rules. */
function hostPathLooksAbsolute(expanded) {
return path.isAbsolute(expanded) || isWindowsDrivePath(expanded);
}
function resolveToCwd(filePath, cwd) {
const expanded = expandPath(filePath);
if (isWindowsDrivePath(expanded)) return path.win32.normalize(expanded);
if (path.isAbsolute(expanded)) return expanded;
return path.resolve(cwd, expanded);
}
function resolveSandboxInputPath(filePath, cwd) {
return resolveToCwd(filePath, cwd);
}
function resolveSandboxPath(params) {
const resolved = resolveSandboxInputPath(params.filePath, params.cwd);
const rootResolved = path.resolve(params.root);
const relative = path.relative(rootResolved, resolved);
if (!relative || relative === "") return {
resolved,
relative: ""
};
if (relative === ".." || relative.startsWith("../") || relative.startsWith("..\\") || path.isAbsolute(relative) || isWindowsDrivePath(relative)) throw new Error(`Path escapes sandbox root (${shortPath(rootResolved)}): ${params.filePath}`);
return {
resolved,
relative
};
}
async function assertSandboxPath(params) {
const resolved = resolveSandboxPath(params);
const policy = {
allowFinalSymlinkForUnlink: params.allowFinalSymlinkForUnlink,
allowFinalHardlinkForUnlink: params.allowFinalHardlinkForUnlink
};
await assertNoPathAliasEscape({
absolutePath: resolved.resolved,
rootPath: params.root,
boundaryLabel: "sandbox root",
policy
});
return resolved;
}
function assertMediaNotDataUrl(media) {
const raw = media.trim();
if (DATA_URL_RE.test(raw)) throw new Error("data: URLs are not supported for media. Use buffer instead.");
}
function isManagedMediaPathUnderRoot(candidate) {
const expanded = expandPath(candidate);
if (!hostPathLooksAbsolute(expanded)) return false;
const mediaRoot = path.join(resolveConfigDir(), "media");
const resolvedMediaRoot = path.resolve(mediaRoot);
const resolvedExpanded = path.resolve(expanded);
if (resolvedExpanded === resolvedMediaRoot || !isPathInside(resolvedMediaRoot, resolvedExpanded)) return false;
const firstSegment = path.relative(resolvedMediaRoot, resolvedExpanded).split(path.sep)[0] ?? "";
return MANAGED_MEDIA_SUBDIRS.has(firstSegment) || firstSegment.startsWith("tool-");
}
async function resolveAllowedManagedMediaPath(candidate) {
const expanded = expandPath(candidate);
if (!isManagedMediaPathUnderRoot(expanded)) return;
const resolved = path.resolve(expanded);
await assertNoManagedMediaAliasEscape({
filePath: resolved,
managedMediaRoot: path.resolve(resolveConfigDir(), "media")
});
return resolved;
}
async function resolveSandboxedMediaSource(params) {
const raw = params.media.trim();
if (!raw) return raw;
if (isPassThroughRemoteMediaSource(raw)) return raw;
let candidate = raw;
if (/^file:\/\//i.test(candidate)) {
const workspaceMappedFromUrl = mapContainerWorkspaceFileUrl({
fileUrl: candidate,
sandboxRoot: params.sandboxRoot
});
if (workspaceMappedFromUrl) candidate = workspaceMappedFromUrl;
else try {
candidate = safeFileURLToPath(candidate);
} catch (err) {
throw new Error(`Invalid file:// URL for sandboxed media: ${err.message}`, { cause: err });
}
}
const containerWorkspaceMapped = mapContainerWorkspacePath({
candidate,
sandboxRoot: params.sandboxRoot
});
if (containerWorkspaceMapped) candidate = containerWorkspaceMapped;
assertNoWindowsNetworkPath(candidate, "Sandbox media path");
const tmpMediaPath = await resolveAllowedTmpMediaPath({
candidate,
sandboxRoot: params.sandboxRoot
});
if (tmpMediaPath) return tmpMediaPath;
const managedMediaPath = await resolveAllowedManagedMediaPath(candidate);
if (managedMediaPath) return managedMediaPath;
return (await assertSandboxPath({
filePath: candidate,
cwd: params.sandboxRoot,
root: params.sandboxRoot
})).resolved;
}
async function assertNoManagedMediaAliasEscape(params) {
await assertNoPathAliasEscape({
absolutePath: params.filePath,
rootPath: params.managedMediaRoot,
boundaryLabel: "managed media root"
});
}
function mapContainerWorkspaceFileUrl(params) {
let parsed;
try {
parsed = new URL(params.fileUrl);
} catch {
return;
}
if (parsed.protocol !== "file:") return;
const host = parsed.hostname.trim().toLowerCase();
if (host && host !== "localhost") return;
if (hasEncodedFileUrlSeparator(parsed.pathname)) return;
let normalizedPathname;
try {
normalizedPathname = decodeURIComponent(parsed.pathname).replace(/\\/g, "/");
} catch {
return;
}
if (normalizedPathname !== SANDBOX_CONTAINER_WORKDIR && !normalizedPathname.startsWith(`${SANDBOX_CONTAINER_WORKDIR}/`)) return;
return mapContainerWorkspacePath({
candidate: normalizedPathname,
sandboxRoot: params.sandboxRoot
});
}
function mapContainerWorkspacePath(params) {
const normalized = params.candidate.replace(/\\/g, "/");
if (normalized === SANDBOX_CONTAINER_WORKDIR) return path.resolve(params.sandboxRoot);
const prefix = `${SANDBOX_CONTAINER_WORKDIR}/`;
if (!normalized.startsWith(prefix)) return;
const rel = normalized.slice(prefix.length);
if (!rel) return path.resolve(params.sandboxRoot);
return path.resolve(params.sandboxRoot, ...rel.split("/").filter(Boolean));
}
async function resolveAllowedTmpMediaPath(params) {
if (!hostPathLooksAbsolute(expandPath(params.candidate))) return;
const resolved = path.resolve(resolveSandboxInputPath(params.candidate, params.sandboxRoot));
const openClawTmpDir = path.resolve(resolvePreferredOpenClawTmpDir());
if (!isPathInside(openClawTmpDir, resolved)) return;
await assertNoTmpAliasEscape({
filePath: resolved,
tmpRoot: openClawTmpDir
});
return resolved;
}
async function assertNoTmpAliasEscape(params) {
await assertNoPathAliasEscape({
absolutePath: params.filePath,
rootPath: params.tmpRoot,
boundaryLabel: "tmp root"
});
}
function shortPath(value) {
if (value.startsWith(os.homedir())) return `~${value.slice(os.homedir().length)}`;
return value;
}
//#endregion
export { resolveSandboxPath as a, resolveSandboxInputPath as i, assertSandboxPath as n, resolveSandboxedMediaSource as o, resolveAllowedManagedMediaPath as r, assertMediaNotDataUrl as t };