UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

268 lines (267 loc) 10.1 kB
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 };