UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

135 lines (134 loc) 6.7 kB
import { P as readFileHandleBounded, t as FsSafeError, u as openLocalFileSafely } from "./fs-safe-B6pvPGnf.js"; import { r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import { t as assertNoWindowsNetworkPath } from "./local-file-access-B1HKoQAc.js"; import { o as resolveInboundMediaReference, t as MediaReferenceError } from "./media-reference-ZG0ZXjCX.js"; import { a as resolveInboundPathRoot } from "./inbound-path-policy-DQ5Rksw7.js"; import { i as getDefaultMediaLocalRoots } from "./local-roots-BphuGw-a.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/media/local-media-access.ts /** Error raised when a local media path escapes the configured allowlist. */ var LocalMediaAccessError = class extends Error { constructor(code, message, options) { super(message, options); this.code = code; this.name = "LocalMediaAccessError"; } }; /** * Lets core classify rejected content without changing loadWebMedia's public error code. * Removing this boundary would make detailed reply outcomes break plugin error handling. */ var HostReadMediaTypeError = class extends LocalMediaAccessError { constructor(message) { super("path-not-allowed", message); } }; /** Returns the default root allowlist for local media reads. */ function getDefaultLocalRootsCore() { return getDefaultMediaLocalRoots(); } async function resolveCanonicalBoundaryPath(root) { const resolved = path.resolve(root); try { return await fs.realpath(resolved); } catch { return resolved; } } /** Resolves an allowlist once for callers that validate several media paths. */ async function resolveLocalMediaRoots(localRoots) { const roots = localRoots ?? getDefaultLocalRootsCore(); return await Promise.all(roots.map(async (root) => { const resolvedRoot = await resolveCanonicalBoundaryPath(root); if (resolvedRoot === path.parse(resolvedRoot).root) throw new LocalMediaAccessError("invalid-root", `Invalid localRoots entry (refuses filesystem root): ${root}. Pass a narrower directory.`); return resolvedRoot; })); } async function resolveLocalMediaPathForContainment(mediaPath) { try { return await fs.realpath(mediaPath); } catch { try { return path.join(await fs.realpath(path.dirname(mediaPath)), path.basename(mediaPath)); } catch { return path.resolve(mediaPath); } } } async function resolveLocalMediaBoundary(mediaPath, localRoots, managedReferenceErrors, options) { if (localRoots === "any") return { rejectHardlinks: false, roots: "any" }; let inboundReference; try { inboundReference = await resolveInboundMediaReference(mediaPath); } catch (err) { if (managedReferenceErrors === "reject" && err instanceof MediaReferenceError) throw new LocalMediaAccessError(err.code, err.message, { cause: err }); if (!(err instanceof MediaReferenceError)) throw err; } if (inboundReference) return { rejectHardlinks: true, roots: await resolveLocalMediaRoots([path.dirname(inboundReference.physicalPath)]) }; try { assertNoWindowsNetworkPath(mediaPath, "Local media path"); } catch (err) { throw new LocalMediaAccessError("network-path-not-allowed", err.message, { cause: err }); } const matchedInboundRoot = options?.inboundRoots?.length ? resolveInboundPathRoot({ filePath: mediaPath, roots: options.inboundRoots }) : void 0; if (matchedInboundRoot) { const resolvedAnchor = await resolveCanonicalBoundaryPath(matchedInboundRoot.anchorRoot); const resolvedRoot = await resolveCanonicalBoundaryPath(matchedInboundRoot.matchedRoot); if (!isPathInside(resolvedAnchor, resolvedRoot)) throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`); return { rejectHardlinks: true, roots: [resolvedRoot] }; } const roots = localRoots ?? getDefaultLocalRootsCore(); const resolved = await resolveLocalMediaPathForContainment(mediaPath); const resolvedRoots = options?.resolvedRoots ?? await options?.resolveRoots?.() ?? await resolveLocalMediaRoots(roots); const workspaceRoot = roots[roots.findIndex((root) => path.basename(root) === "workspace")]; if (workspaceRoot) { const stateDir = await resolveCanonicalBoundaryPath(path.dirname(workspaceRoot)); const rel = path.relative(stateDir, resolved); const firstSegment = rel.split(path.sep)[0] ?? ""; if (rel && isPathInside(stateDir, resolved) && firstSegment.startsWith("workspace-")) { const agentWorkspace = path.join(stateDir, firstSegment); if (!(localRoots !== void 0 && resolvedRoots.some((root) => isPathInside(agentWorkspace, root) && isPathInside(root, resolved)))) throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`); } } for (const [index, resolvedRoot] of resolvedRoots.entries()) { const root = roots[index] ?? resolvedRoot; if (resolvedRoot === path.parse(resolvedRoot).root) throw new LocalMediaAccessError("invalid-root", `Invalid localRoots entry (refuses filesystem root): ${root}. Pass a narrower directory.`); if (isPathInside(resolvedRoot, resolved)) return { rejectHardlinks: false, roots: resolvedRoots }; } throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`); } /** Verifies that a local media path is managed inbound media or lives under allowed roots. */ async function assertLocalMediaAllowed(mediaPath, localRoots, options) { await resolveLocalMediaBoundary(mediaPath, localRoots, "ignore", options); } /** Opens, revalidates, and bounded-reads local media against one frozen root boundary. */ async function readLocalMediaFile(mediaPath, localRoots, options) { const boundary = await resolveLocalMediaBoundary(mediaPath, localRoots, "reject", options); const opened = await openLocalFileSafely({ filePath: mediaPath }); try { if (boundary.roots !== "any" && !boundary.roots.some((resolvedRoot) => isPathInside(resolvedRoot, opened.realPath))) throw new LocalMediaAccessError("path-not-allowed", `Local media path is not under an allowed directory: ${mediaPath}`); if (boundary.rejectHardlinks && opened.stat.nlink > 1) throw new FsSafeError("hardlink", "hardlinked path not allowed"); if (opened.stat.size > options.maxBytes) throw new FsSafeError("too-large", `file exceeds limit of ${options.maxBytes} bytes (got ${opened.stat.size})`); return await readFileHandleBounded(opened.handle, options.maxBytes); } finally { await opened.handle.close().catch(() => {}); } } //#endregion export { readLocalMediaFile as a, getDefaultLocalRootsCore as i, LocalMediaAccessError as n, resolveLocalMediaRoots as o, assertLocalMediaAllowed as r, HostReadMediaTypeError as t };