openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
187 lines (186 loc) • 8.5 kB
JavaScript
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 };