UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

187 lines (186 loc) 8.5 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { r as isMissingPathError } from "./errno-CkbDOfLk.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { r as SKILL_LIBRARY_MAX_FILE_BYTES } from "./skill-library-s2fFtoG-.js"; import { n as createSyntheticSourceInfo } from "./source-info-CcFiWAof.js"; import { t as formatSkillsForPromptBounded } from "./skill-prompt-limits-AF9diYak.js"; import { a as shouldSyncSkillPath, t as loadSingleSkillDirectory } from "./local-loader-D2Y-takC.js"; import { E as readSkillBundleTree, M as SkillLibraryError, S as SkillTreeDirectoryError, i as readSelectedSkillLibraryFiles, r as loadSkillLibrarySelection, w as prepareSkillBundle } from "./selection-NYtFFG08.js"; import { t as runBestEffortCleanup } from "./non-fatal-cleanup-Dsr9pn7u.js"; import { n as SkillResourceDeliverySchema } from "./skill-resources-CDw_zmre.js"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; import { Value } from "typebox/value"; //#region src/infra/temp-artifact-cleanup.ts const log$1 = createSubsystemLogger("infra:temp-artifacts"); function removeTemporaryArtifacts(directory, owner) { return runBestEffortCleanup({ cleanup: () => fs.rm(directory, { recursive: true, force: true }), onError: (error) => log$1.warn(truncateUtf16Safe(formatErrorMessage(`${owner} cleanup failed; files may remain in ${directory}. After the worker or session stops, check permissions and remove the retained directory: ${formatErrorMessage(error)}`), 1024)) }); } //#endregion //#region src/skills/runtime/resources.ts const log = createSubsystemLogger("skills/resources"); function contextualizeSkillResourceError(skill, error) { const detail = error instanceof Error ? error.message : String(error); const message = `Failed to prepare skill resources: skill=${JSON.stringify(skill.name)} root=${JSON.stringify(skill.baseDir)} error=${detail}`; if (error instanceof SkillLibraryError) return new SkillLibraryError(error.code, message, error.currentRevision, { cause: error }); return new SkillLibraryError("INVALID_BUNDLE", message, void 0, { cause: error }); } function isMissingDiscoveredSkillRoot(error) { return error instanceof SkillTreeDirectoryError && path.resolve(error.failedPath) === path.resolve(error.rootPath) && isMissingPathError(error.cause); } async function prepareSkillResourceDelivery(snapshot, assertCurrent, explicitSelections = []) { if (!snapshot) return; assertCurrent(); if (!snapshot.resolvedSkills?.length && !snapshot.librarySelections?.length && !explicitSelections.length) return; const skills = []; let total = 0; const candidates = [...snapshot.resolvedSkills ?? []]; for (const entry of loadSkillLibrarySelection(snapshot.librarySelections ?? [])) if (snapshot.skills.some((skill) => skill.name === entry.skill.name) && !candidates.some((skill) => skill.name === entry.skill.name)) candidates.push(entry.skill); for (const selected of explicitSelections) { if (selected.path.startsWith("node://") || candidates.some((skill) => skill.filePath === selected.path)) continue; const skillDir = path.dirname(selected.path); let rootRealPath; try { rootRealPath = await fs.realpath(skillDir); } catch (error) { throw contextualizeSkillResourceError({ name: selected.name, baseDir: skillDir }, error); } assertCurrent(); const loaded = loadSingleSkillDirectory({ skillDir, rootRealPath, source: "openclaw-resources", maxBytes: SKILL_LIBRARY_MAX_FILE_BYTES }); if (!loaded || loaded.skill.filePath !== selected.path || !snapshot.skills.some((skill) => skill.name === loaded.skill.name) || candidates.some((skill) => skill.name === loaded.skill.name)) throw new Error(`Explicit skill no longer matches the prepared catalog: skill=${JSON.stringify(selected.name)} root=${JSON.stringify(skillDir)} path=${JSON.stringify(selected.path)}. Refresh skill selection and retry.`); candidates.push(loaded.skill); } for (const skill of candidates) { if (skill.filePath.startsWith("node://")) continue; const pin = snapshot.librarySelections?.find((selection) => selection.name === skill.name); const explicitlySelected = explicitSelections.some((selection) => selection.path === skill.filePath); let files; try { files = pin ? await readSelectedSkillLibraryFiles(pin) : await readSkillBundleTree(skill.baseDir, shouldSyncSkillPath); } catch (error) { if (!pin && !explicitlySelected && isMissingDiscoveredSkillRoot(error)) { assertCurrent(); log.warn("Skipping stale discovered skill during worker resource preparation.", { skill: skill.name, root: skill.baseDir, failedPath: error.failedPath, error: error.message }); continue; } throw contextualizeSkillResourceError(skill, error); } assertCurrent(); let bundle; try { bundle = prepareSkillBundle(files); } catch (error) { throw contextualizeSkillResourceError(skill, error); } total += bundle.files.reduce((sum, file) => sum + file.sizeBytes, 0); if (total > 8388608) throw new Error("Selected skill resources exceed the worker delivery limit (8 MiB). Select fewer skills before retrying."); skills.push({ name: skill.name, sourcePath: skill.filePath, modelVisible: (snapshot.resolvedSkills?.some((selected) => selected.filePath === skill.filePath) ?? false) || explicitSelections.some((selected) => selected.path === skill.filePath), ...skill.displayName ? { displayName: skill.displayName } : {}, description: skill.description, revision: bundle.revision, files }); } const delivery = { version: 1, skills }; if (!Value.Check(SkillResourceDeliverySchema, delivery)) throw new Error("Selected skill catalog exceeds the worker resource contract."); return delivery; } /** Owns private turn inputs independently of credential-bearing worker state. */ async function materializeSkillResources(delivery, assertCurrent) { if (!Value.Check(SkillResourceDeliverySchema, delivery)) throw new Error("Invalid skill resource delivery."); const bundles = delivery.skills.map((skill) => ({ skill, bundle: prepareSkillBundle(skill.files) })); if (bundles.some(({ skill, bundle }) => skill.revision !== bundle.revision) || bundles.reduce((sum, { bundle }) => sum + bundle.files.reduce((bytes, file) => bytes + file.sizeBytes, 0), 0) > 8388608) throw new Error("Skill resource integrity or delivery limit check failed."); assertCurrent(); const directory = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-resources-")); const cleanup = () => removeTemporaryArtifacts(directory, "Materialized skill"); try { const pathMappings = []; const resolvedSkills = []; for (const [index, { skill, bundle }] of bundles.entries()) { const baseDir = path.join(directory, String(index)); for (const file of bundle.files) { assertCurrent(); const target = path.join(baseDir, file.path); await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 }); assertCurrent(); await fs.writeFile(target, file.bytes, { mode: file.executable ? 320 : 256, flag: "wx" }); } const filePath = path.join(baseDir, "SKILL.md"); if (skill.sourcePath) { pathMappings.push([skill.sourcePath, filePath]); pathMappings.push([skill.sourcePath.slice(0, -8), `${baseDir}${path.sep}`]); } resolvedSkills.push({ name: skill.name, displayName: skill.displayName, description: skill.description, filePath, baseDir, source: "openclaw-resources", sourceInfo: createSyntheticSourceInfo(filePath, { source: "openclaw-resources", baseDir }), disableModelInvocation: skill.modelVisible === false }); } assertCurrent(); return { directory, snapshot: { skills: resolvedSkills.map((skill) => ({ name: skill.name, skillKey: skill.name })), resolvedSkills, prompt: formatSkillsForPromptBounded({ skills: resolvedSkills.filter((skill) => !skill.disableModelInvocation), preserveOrder: true }) }, rewriteReferences: (text) => pathMappings.reduce((rewritten, [source, target]) => rewritten.replaceAll(source, target), text), cleanup }; } catch (error) { await cleanup(); throw error; } } //#endregion export { prepareSkillResourceDelivery as n, removeTemporaryArtifacts as r, materializeSkillResources as t };