UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

170 lines (169 loc) • 6.71 kB
import { l as extractArchive, r as ArchiveLimitError, t as ARCHIVE_LIMIT_ERROR_CODE } from "./archive-DqZZMnnW.mjs"; import { a as wrapExternalContent } from "./external-content-Bkm4w5qo.mjs"; import "./security-runtime-Cl_Gnohk.mjs"; import { n as textResult } from "./tool-results-BCM3fdVS.mjs"; import { f as saveMediaBuffer } from "./store-CP-GWHOW.mjs"; import "./archive-DEZkiUYO.mjs"; import "./media-store-DzrxDeHE.mjs"; import { a as FILE_TRANSFER_SUBDIR, t as DIR_FETCH_TOOL_DESCRIPTOR } from "./descriptors-D69ns4FR.mjs"; import { i as DIR_FETCH_HARD_MAX_BYTES, r as DIR_FETCH_DEFAULT_MAX_BYTES, t as DIR_FETCH_ARCHIVE_POLICY } from "./dir-fetch-archive-BFog-jM-.mjs"; import { r as mimeFromExtension, t as IMAGE_MIME_INLINE_SET } from "./mime-2T0HwsBn.mjs"; import { i as readClampedInt, n as readRequiredNodePath, t as invokeNodeToolPayload } from "./node-tool-invoke-DbfHwdQE.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; import { walkDirectory } from "@openclaw/fs-safe/walk"; import { sha256File } from "@openclaw/fs-safe/durability"; //#region extensions/file-transfer/src/tools/dir-fetch-tool.ts const MEDIA_URL_CAP = 25; const DIRECTORY_TEXT_MAX_BYTES = 8192; const TAR_UNPACK_TIMEOUT_MS = 6e4; function classifyArchiveFailure(error) { const reason = error instanceof Error ? error.message : String(error); if (error instanceof ArchiveLimitError && error.code !== ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT) return { auditCode: "TREE_TOO_LARGE", publicCode: "UNCOMPRESSED_TOO_LARGE", reason }; return { auditCode: "UNSAFE_ARCHIVE", publicCode: "UNSAFE_ARCHIVE", reason }; } function savedDirectoryText(rootDir, files) { const header = JSON.stringify({ rootDir, fileCount: files.length }).slice(0, -1); const visible = []; const render = () => { const manifest = `${header},"displayedCount":${visible.length},"files":[${visible.join(",")}]}`; const note = `${files.length - visible.length} saved files omitted from this text (byte limit or reserved path markers). All remain under rootDir; inspect them with available local file or directory capabilities.`; const wrapped = wrapExternalContent(`Fetched ${files.length} files.\n${manifest}\n${note}`, { source: "unknown" }); return wrapped.includes(manifest) && Buffer.byteLength(wrapped, "utf8") <= DIRECTORY_TEXT_MAX_BYTES ? wrapped : void 0; }; let text = render(); for (const { relPath, size } of files.toSorted((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0)) { visible.push(JSON.stringify({ relPath, size })); const candidate = render(); if (!candidate) break; text = candidate; } return text ?? wrapExternalContent(`Fetched ${files.length} files. Saved paths omitted: rootDir cannot be represented safely within the 8192-byte text limit. No usable local path is shown.`, { source: "unknown" }); } function createDirFetchTool() { return { ...DIR_FETCH_TOOL_DESCRIPTOR, execute: async (_toolCallId, args) => { const params = args; const { node, requestedPath: dirPath } = readRequiredNodePath(params); const maxBytes = readClampedInt({ input: params, key: "maxBytes", defaultValue: DIR_FETCH_DEFAULT_MAX_BYTES, hardMin: 1, hardMax: DIR_FETCH_HARD_MAX_BYTES }); const { audit, payload } = await invokeNodeToolPayload({ node, params, command: "dir.fetch", commandParams: { path: dirPath, maxBytes }, requestedPath: dirPath }); const canonicalPath = typeof payload.path === "string" ? payload.path : ""; const tarBase64 = typeof payload.tarBase64 === "string" ? payload.tarBase64 : ""; const tarBytes = typeof payload.tarBytes === "number" ? payload.tarBytes : -1; const sha256 = typeof payload.sha256 === "string" ? payload.sha256 : ""; if (!canonicalPath || !tarBase64 || tarBytes < 0 || !sha256) throw new Error("invalid dir.fetch payload (missing fields)"); const tarBuffer = Buffer.from(tarBase64, "base64"); if (tarBuffer.byteLength !== tarBytes) throw new Error(`dir.fetch size mismatch: payload says ${tarBytes} bytes, decoded ${tarBuffer.byteLength}`); if (crypto.createHash("sha256").update(tarBuffer).digest("hex") !== sha256) throw new Error("dir.fetch sha256 mismatch (integrity failure)"); const savedTar = await saveMediaBuffer(tarBuffer, "application/gzip", FILE_TRANSFER_SUBDIR, DIR_FETCH_HARD_MAX_BYTES); const tarDir = path.dirname(savedTar.path); const unpackId = `dir-fetch-${path.basename(savedTar.path, path.extname(savedTar.path))}`; const rootDir = path.join(tarDir, unpackId); await fs.mkdir(rootDir, { recursive: true, mode: 448 }); try { await extractArchive({ archivePath: savedTar.path, destDir: rootDir, kind: "tar", tarGzip: true, timeoutMs: TAR_UNPACK_TIMEOUT_MS, entryModes: "clamp", ...DIR_FETCH_ARCHIVE_POLICY }); } catch (error) { await Promise.all([fs.rm(rootDir, { recursive: true, force: true }).catch(() => void 0), fs.rm(savedTar.path, { force: true }).catch(() => void 0)]); const failure = classifyArchiveFailure(error); await audit({ canonicalPath, decision: "error", errorCode: failure.auditCode, errorMessage: failure.reason, sizeBytes: tarBytes, sha256 }); throw new Error(`dir.fetch ${failure.publicCode}: ${failure.reason}`, { cause: error }); } const walked = await walkDirectory(rootDir, { symlinks: "skip", include: ({ kind }) => kind === "file" }); if (walked.failedDirs.length > 0) throw walked.failedDirs[0].error; const files = []; for (const { relativePath: relPath, path: absPath } of walked.entries) { let size; try { size = (await fs.stat(absPath)).size; } catch { continue; } const mimeType = mimeFromExtension(relPath); const fileSha256 = (await sha256File(absPath)).digest; files.push({ relPath, size, mimeType, sha256: fileSha256, localPath: absPath }); } const fileCount = files.length; const imageFiles = files.filter((f) => IMAGE_MIME_INLINE_SET.has(f.mimeType)); const nonImageFiles = files.filter((f) => !IMAGE_MIME_INLINE_SET.has(f.mimeType)); const mediaUrls = [...imageFiles, ...nonImageFiles].slice(0, MEDIA_URL_CAP).map((f) => f.localPath); await audit({ canonicalPath, decision: "allowed", sizeBytes: tarBytes, sha256 }); return textResult(savedDirectoryText(rootDir, files), { path: canonicalPath, rootDir, fileCount, tarBytes, sha256, files, media: { mediaUrls } }); } }; } //#endregion export { createDirFetchTool };