UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

164 lines (163 loc) • 8.22 kB
import { t as FsSafeError, u as openLocalFileSafely } from "./fs-safe-BUXBSN-4.mjs"; import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs"; import { t as _usingCtx } from "./usingCtx-DwOBYOJf.mjs"; import { n as captureChannelReadScope } from "./channel-read-authority-CyorchNP.mjs"; import { t as MediaReferenceError } from "./inbound-media-uri-Bc4RWtcM.mjs"; import { a as resolveInboundPathRoot } from "./inbound-path-policy-BLgg6tDE.mjs"; import { i as getDefaultMediaLocalRoots } from "./local-roots-D6JiS64U.mjs"; import { r as resolveInboundMediaReference } from "./media-reference-C8XnD_RC.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import { assertNoWindowsNetworkPath, readFileHandleBounded } from "@openclaw/fs-safe/advanced"; //#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); } }; 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 ?? getDefaultMediaLocalRoots(); 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 ?? getDefaultMediaLocalRoots(); 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 local media against one frozen root boundary without buffering its contents. */ async function openLocalMediaFile(mediaPath, localRoots, options) { const readScope = captureChannelReadScope(); readScope?.assertCurrent(); const boundary = await resolveLocalMediaBoundary(mediaPath, localRoots, "reject", options); const excludedRoots = options.excludedRoots?.length ? await resolveLocalMediaRoots(options.excludedRoots) : []; readScope?.assertCurrent(); const opened = await openLocalFileSafely({ filePath: mediaPath }); try { if (excludedRoots.some((root) => isPathInside(root, opened.realPath))) throw new LocalMediaAccessError("path-not-allowed", `Local media path belongs to a remote workspace: ${mediaPath}`); 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})`); readScope?.assertCurrent(); return opened; } catch (error) { await opened[Symbol.asyncDispose](); throw error; } } /** Opens, revalidates, and bounded-reads local media against one frozen root boundary. */ async function readLocalMediaFile(mediaPath, localRoots, options) { try { var _usingCtx$1 = _usingCtx(); const readScope = captureChannelReadScope(); const opened = _usingCtx$1.a(await openLocalMediaFile(mediaPath, localRoots, options)); if (!readScope) return await readFileHandleBounded(opened.handle, options.maxBytes); const guardedHandle = { fd: opened.handle.fd, async read(bufferOrOptions, offsetOrOptions, length, position) { readScope.assertCurrent(); const result = ArrayBuffer.isView(bufferOrOptions) ? typeof offsetOrOptions === "object" && offsetOrOptions !== null ? await opened.handle.read(bufferOrOptions, offsetOrOptions) : await opened.handle.read(bufferOrOptions, offsetOrOptions, length, position) : await opened.handle.read(bufferOrOptions); readScope.assertCurrent(); return result; } }; return await readFileHandleBounded(guardedHandle, options.maxBytes); } catch (_) { _usingCtx$1.e = _; } finally { await _usingCtx$1.d(); } } //#endregion export { readLocalMediaFile as a, openLocalMediaFile as i, LocalMediaAccessError as n, resolveLocalMediaRoots as o, assertLocalMediaAllowed as r, HostReadMediaTypeError as t };