openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
268 lines (267 loc) • 10.1 kB
JavaScript
import { c as getPluginMetadataSnapshotCache, f as withPluginCache } from "./plugin-cache-DGWspMEc.js";
import { r as isMissingPathError } from "./errno-CkbDOfLk.js";
import { n as isPathInside } from "./path-safety-Bi0ppMWC.js";
import "./redact-BtvPPfTi.js";
import { t as CONFIG_DIR } from "./utils-P__uGsPB.js";
import "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { n as registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle-C6m-q5Au.js";
import { a as pluginCacheRealpathSync, i as pluginCacheLstatSync, r as pluginCacheExistsSync, s as readPluginCacheDirectory } from "./plugin-cache-files-DLPF_Tw2.js";
import { r as hasKind } from "./slots-CQdAEuat.js";
import { p as resolveMemorySlotDecision } from "./config-state-BkU1frVq.js";
import { t as shouldRejectHardlinkedPluginFiles } from "./hardlink-policy-CslDqyMD.js";
import { i as resolvePolicyPluginActivationState, r as normalizePluginsConfigWithResolver } from "./manifest-registry-DCCgYk7q.js";
import { c as resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot-w-4EjxI4.js";
import { t as isAcpRuntimeSpawnAvailable } from "./availability-B71AKwcQ.js";
import fs from "node:fs";
import path from "node:path";
//#region src/skills/loading/plugin-skills.ts
const log = createSubsystemLogger("skills");
let lastDefaultPluginSkillsPublication = null;
registerPluginMetadataProcessMemoLifecycleClear(() => {
lastDefaultPluginSkillsPublication = null;
});
function resolvePluginSkillRoots(params) {
const workspaceDir = (params.workspaceDir ?? "").trim();
if (!workspaceDir) {
publishPluginSkills([], { pluginSkillsDir: params.pluginSkillsDir });
return [];
}
const metadataSnapshot = resolvePluginMetadataSnapshot({
workspaceDir,
config: params.config,
env: process.env
});
return resolvePluginSkillRootsFromMetadata({
...params,
metadataSnapshot
});
}
function resolvePluginSkillRootsFromMetadata(params) {
return withPluginCache(getPluginMetadataSnapshotCache(params.metadataSnapshot), () => resolvePluginSkillRootsInOwner(params));
}
function resolvePluginSkillRootsInOwner(params) {
if (!(params.workspaceDir ?? "").trim()) {
publishPluginSkills([], { pluginSkillsDir: params.pluginSkillsDir });
return [];
}
const config = params.config ?? {};
const metadataSnapshot = params.metadataSnapshot;
const registry = metadataSnapshot.manifestRegistry;
if (registry.plugins.length === 0) {
publishPluginSkills([], { pluginSkillsDir: params.pluginSkillsDir });
return [];
}
const acpRuntimeAvailable = isAcpRuntimeSpawnAvailable({ config });
const normalizedPlugins = normalizePluginsConfigWithResolver(config.plugins, metadataSnapshot.normalizePluginId);
const memorySlot = normalizedPlugins.slots.memory;
let selectedMemoryPluginId = null;
const seen = /* @__PURE__ */ new Set();
const resolved = [];
for (const record of registry.plugins) {
if (!record.skills || record.skills.length === 0) continue;
if (!resolvePolicyPluginActivationState({
id: record.id,
origin: record.origin,
channelIds: record.channels,
config: normalizedPlugins,
rootConfig: config,
enabledByDefault: record.enabledByDefault
}).activated) continue;
if (!acpRuntimeAvailable && record.id === "acpx") continue;
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId
});
if (!memoryDecision.enabled) continue;
if (memoryDecision.selected && hasKind(record.kind, "memory")) selectedMemoryPluginId = record.id;
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: record.origin,
rootDir: record.rootDir
});
for (const raw of record.skills) {
const trimmed = raw.trim();
if (!trimmed) continue;
const candidate = path.resolve(record.rootDir, trimmed);
if (!pluginCacheExistsSync(candidate)) {
log.warn(`plugin skill path not found (${record.id}): ${candidate}`);
continue;
}
if (!isPluginSkillPathInside(record.rootDir, candidate)) {
log.warn(`plugin skill path escapes plugin root (${record.id}): ${candidate}`);
continue;
}
const candidates = record.bundleFormat === "agent" ? collectAgentSkillTargets(candidate) : [candidate];
for (const resolvedCandidate of candidates) {
if (seen.has(resolvedCandidate)) continue;
seen.add(resolvedCandidate);
resolved.push({
dir: resolvedCandidate,
rejectHardlinks
});
}
}
}
publishPluginSkills(resolved.map((root) => root.dir), { pluginSkillsDir: params.pluginSkillsDir });
return resolved;
}
function isPluginSkillPathInside(rootDir, candidate) {
if (!isPathInside(rootDir, candidate)) return false;
const rootRealPath = pluginCacheRealpathSync(rootDir);
const candidateRealPath = pluginCacheRealpathSync(candidate);
return Boolean(rootRealPath && candidateRealPath && isPathInside(rootRealPath, candidateRealPath));
}
function listSkillChildDirectories(dir) {
try {
return readPluginCacheDirectory(dir).filter((entry) => entry.isDirectory()).map((entry) => ({
name: entry.name,
path: path.join(dir, entry.name)
}));
} catch {
return [];
}
}
function collectAgentSkillTargets(skillsRoot) {
const targets = [];
for (const entry of listSkillChildDirectories(skillsRoot)) {
if (hasPublishableSkillFile({
skillDir: entry.path,
rootDir: skillsRoot
})) {
targets.push(entry.path);
continue;
}
log.warn(`agent plugin skill skipped because SKILL.md is missing or invalid: ${entry.path}`);
}
return targets;
}
function resolveDefaultPluginSkillsDir() {
return path.join(CONFIG_DIR, "plugin-skills");
}
function resolvePluginSkillLinkType(platform = process.platform) {
return platform === "win32" ? "junction" : "dir";
}
/**
* Collect skill dir targets from a resolved directory.
* If the directory contains a direct SKILL.md it is published as-is.
* Otherwise child subdirectories that contain SKILL.md are expanded.
*/
function collectSkillTargets(dir, targets) {
if (hasPublishableSkillFile({
skillDir: dir,
rootDir: dir
})) {
const basename = path.basename(dir);
const existing = targets.get(basename);
if (existing) {
log.warn(`plugin skill name collision: "${basename}" resolves to both ${existing} and ${dir}; only the first will be published`);
return;
}
targets.set(basename, dir);
return;
}
for (const entry of listSkillChildDirectories(dir)) {
const childPath = entry.path;
if (!hasPublishableSkillFile({
skillDir: childPath,
rootDir: dir
})) continue;
const basename = entry.name;
const existing = targets.get(basename);
if (existing) {
log.warn(`plugin skill name collision: "${basename}" resolves to both ${existing} and ${childPath}; only the first will be published`);
continue;
}
targets.set(basename, childPath);
}
}
function hasPublishableSkillFile(params) {
const skillMd = path.join(params.skillDir, "SKILL.md");
const skillMdStat = pluginCacheLstatSync(skillMd);
if (!skillMdStat) return false;
if (!skillMdStat.isFile() || skillMdStat.isSymbolicLink()) {
log.warn(`plugin skill SKILL.md is not a regular file: ${skillMd}`);
return false;
}
if (!isPluginSkillPathInside(params.rootDir, skillMd)) {
log.warn(`plugin skill SKILL.md escapes declared skill root: ${skillMd}`);
return false;
}
return true;
}
/**
* Creates symlinks from each resolved plugin skill directory into the
* plugin skills directory (~/.openclaw/plugin-skills/) so the agent SDK can
* discover them at the conventional file-system path.
*
* The plugin-skills directory is fully owned by OpenClaw — every entry is
* a generated symlink. Cleanup of stale links is therefore safe.
*/
function publishPluginSkills(skillDirs, opts) {
const pluginSkillsDir = opts?.pluginSkillsDir ?? resolveDefaultPluginSkillsDir();
const managedTargets = /* @__PURE__ */ new Map();
for (const dir of skillDirs) collectSkillTargets(dir, managedTargets);
if (opts?.pluginSkillsDir === void 0 && lastDefaultPluginSkillsPublication?.size === managedTargets.size && [...managedTargets].every(([name, target]) => lastDefaultPluginSkillsPublication?.get(name) === target)) return;
for (const [name, target] of managedTargets) {
const linkPath = path.join(pluginSkillsDir, name);
try {
fs.mkdirSync(pluginSkillsDir, { recursive: true });
} catch {}
try {
const existingEntry = fs.lstatSync(linkPath);
if (existingEntry.isSymbolicLink()) {
if (fs.readlinkSync(linkPath) === target) continue;
removeGeneratedPluginSkillEntry(linkPath);
} else if (isGeneratedPluginSkillEntry(existingEntry)) removeGeneratedPluginSkillEntry(linkPath);
else {
log.warn(`plugin skill entry is not a generated symlink: ${linkPath}`);
continue;
}
} catch (err) {
if (!isMissingPathError(err)) {
log.warn(`failed to inspect plugin skill symlink "${linkPath}": ${String(err)}`);
continue;
}
}
try {
fs.symlinkSync(target, linkPath, resolvePluginSkillLinkType());
} catch (err) {
log.warn(`failed to create plugin skill symlink "${linkPath}" → "${target}": ${String(err)}`);
}
}
let existingEntries;
try {
existingEntries = fs.readdirSync(pluginSkillsDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of existingEntries) {
if (!isGeneratedPluginSkillEntry(entry)) continue;
if (managedTargets.has(entry.name)) continue;
removeGeneratedPluginSkillEntry(path.join(pluginSkillsDir, entry.name));
}
if (opts?.pluginSkillsDir === void 0) lastDefaultPluginSkillsPublication = managedTargets;
}
function isGeneratedPluginSkillEntry(entry) {
return entry.isSymbolicLink() || process.platform === "win32" && entry.isDirectory();
}
function removeGeneratedPluginSkillEntry(linkPath) {
try {
if (fs.lstatSync(linkPath).isSymbolicLink()) {
fs.unlinkSync(linkPath);
return;
}
} catch (err) {
if (isMissingPathError(err)) return;
}
try {
fs.rmSync(linkPath, {
recursive: true,
force: true
});
} catch {}
}
//#endregion
export { resolvePluginSkillRootsFromMetadata as n, resolvePluginSkillRoots as t };