UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

129 lines (128 loc) • 6.34 kB
import { O as walkDirectory, d as pathExists, w as root } from "./fs-safe-BUXBSN-4.mjs"; import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs"; import { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs"; import "./crypto-digest-y6oZN-_F.mjs"; import { M as stripProposalFrontmatterForSkill } from "./store-D9K8vtNx.mjs"; import path from "node:path"; import { isUtf8 } from "node:buffer"; //#region src/skills/workshop/proposal-bundle.ts const MAX_EVALUATION_FILES = 256; const MAX_EVALUATION_FILE_BYTES = 1048576; const MAX_EVALUATION_BUNDLE_BYTES = 8388608; const MAX_EVALUATION_PATH_DEPTH = 16; const EXCLUDED_ROOT_DIRS = /* @__PURE__ */ new Set([ ".clawhub", ".clawdhub", ".openclaw" ]); async function buildSkillProposalEvaluationBundles(params) { const targetFiles = await readSkillTreeFiles(params.proposal.record.target.skillDir); const targetTreeSha256 = hashSkillTree(targetFiles); const skillMdPath = params.proposal.record.kind === "create" ? "SKILL.md" : resolveTargetSkillRelativePath(params.proposal, targetFiles, { recordedTargetExists: await pathExists(params.proposal.record.target.skillFile) }); const candidateSkillMd = fileFromBuffer(skillMdPath, Buffer.from(stripProposalFrontmatterForSkill(params.proposal.content), "utf8")); const proposedFiles = params.supportFiles.map((file) => fileFromBuffer(file.path, Buffer.from(file.content, "utf8"))); let baseline; if (params.proposal.record.kind === "create") { if (await pathExists(params.proposal.record.target.skillFile)) throw new Error(`Target skill already exists: ${params.proposal.record.target.skillFile}`); for (const file of proposedFiles) { const targetFile = path.join(params.proposal.record.target.skillDir, file.path); if (await pathExists(targetFile)) throw new Error(`Target support file already exists: ${targetFile}`); } } else baseline = snapshotFromFiles(targetFiles, skillMdPath); const candidateFiles = new Map([ ...targetFiles, candidateSkillMd, ...proposedFiles ].map((file) => [file.path, file])); return { ...baseline ? { baseline } : {}, candidate: snapshotFromFiles([...candidateFiles.values()], skillMdPath), targetTreeSha256 }; } async function readSkillProposalTargetTreeSha256(skillDir, options = {}) { return hashSkillTree(await readSkillTreeFiles(skillDir, options.includeRootMetadata)); } async function readSkillTreeFiles(skillDir, includeRootMetadata = false) { const include = (entry) => includeRootMetadata || entry.depth > 1 || !EXCLUDED_ROOT_DIRS.has(entry.name); const scanned = await walkDirectory(skillDir, { maxDepth: 17, maxEntries: MAX_EVALUATION_FILES * 2, symlinks: "include", include, descend: include }); if (scanned.truncated || scanned.entries.some((entry) => entry.depth > MAX_EVALUATION_PATH_DEPTH)) throw new Error("Skill evaluation bundle exceeds traversal limits."); const failed = scanned.failedDirs[0]; if (failed) { if (!failed.relativePath && hasErrnoCode(failed.error, "ENOENT")) return []; throw failed.error; } const skillRoot = await root(skillDir); const files = []; let totalBytes = 0; for (const entry of scanned.entries.toSorted((a, b) => a.relativePath.localeCompare(b.relativePath))) { if (entry.kind === "directory") continue; const portablePath = entry.relativePath.split(path.sep).join("/"); if (entry.kind !== "file") throw new Error(`Skill evaluation bundle contains unsupported entry: ${portablePath}`); const read = await skillRoot.read(entry.relativePath, { hardlinks: "reject", maxBytes: MAX_EVALUATION_FILE_BYTES, symlinks: "reject" }); totalBytes += read.buffer.byteLength; if (totalBytes > MAX_EVALUATION_BUNDLE_BYTES) throw new Error(`Skill evaluation bundle exceeds ${MAX_EVALUATION_BUNDLE_BYTES} total bytes.`); files.push(fileFromBuffer(portablePath, read.buffer)); } return files; } function fileFromBuffer(relativePath, content) { const encoding = !content.includes(0) && isUtf8(content) ? "utf8" : "base64"; return { path: relativePath, content: content.toString(encoding), encoding, sha256: sha256Hex(content), sizeBytes: content.byteLength }; } function snapshotFromFiles(inputFiles, skillMdPath) { const files = inputFiles.toSorted((a, b) => a.path.localeCompare(b.path)); assertEvaluationBundleWithinLimits(files); const skillMd = files.find((file) => file.path === skillMdPath); if (!skillMd) throw new Error(`Skill evaluation bundle is missing ${skillMdPath}.`); return { skillMd, files: files.filter((file) => file.path !== skillMdPath), treeSha256: hashSkillTree(files) }; } function assertEvaluationBundleWithinLimits(files) { if (files.length > MAX_EVALUATION_FILES) throw new Error(`Skill evaluation bundle exceeds ${MAX_EVALUATION_FILES} files.`); let totalBytes = 0; for (const file of files) { if (file.sizeBytes > MAX_EVALUATION_FILE_BYTES) throw new Error(`Skill evaluation bundle file exceeds ${MAX_EVALUATION_FILE_BYTES} bytes: ${file.path}.`); totalBytes += file.sizeBytes; } if (totalBytes > MAX_EVALUATION_BUNDLE_BYTES) throw new Error(`Skill evaluation bundle exceeds ${MAX_EVALUATION_BUNDLE_BYTES} total bytes.`); } function hashSkillTree(files) { return sha256Hex(JSON.stringify(files.toSorted((a, b) => a.path.localeCompare(b.path)).map((file) => ({ path: file.path, sha256: file.sha256, sizeBytes: file.sizeBytes })))); } function resolveTargetSkillRelativePath(proposal, targetFiles, options) { const relativePath = path.relative(path.resolve(proposal.record.target.skillDir), path.resolve(proposal.record.target.skillFile)); if (!relativePath || path.isAbsolute(relativePath) || relativePath.startsWith(`..${path.sep}`)) throw new Error("Skill evaluation target file must be inside the skill directory."); const portablePath = relativePath.split(path.sep).join("/"); if (targetFiles.some((file) => file.path === portablePath)) return portablePath; if (!options.recordedTargetExists) return portablePath; const caseMatches = targetFiles.filter((file) => file.path.toLowerCase() === portablePath.toLowerCase()); if (caseMatches.length === 1) return caseMatches[0].path; if (caseMatches.length > 1) throw new Error(`Skill evaluation target filename is ambiguous: ${portablePath}.`); return portablePath; } //#endregion export { readSkillProposalTargetTreeSha256 as n, buildSkillProposalEvaluationBundles as t };