UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,001 lines (1,000 loc) • 40.7 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs"; import { k as withPluginCache, p as getPluginMetadataSnapshotCache } from "./plugin-cache-CfBhQFSN.mjs"; import { t as containsAsciiControlCharacter } from "./string-normalization-D6DKCpwE.mjs"; import { t as FsSafeError, w as root } from "./fs-safe-BBzelG8F.mjs"; import { r as isMissingPathError } from "./errno-FR4CRyya.mjs"; import { p as isPathInside } from "./includes-B6CYuWNb.mjs"; import { r as isPathInside$1 } from "./path-guards-CH7noY0Z.mjs"; import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs"; import "./paths-BP0DYpE8.mjs"; import "./errors-BvOkmQM5.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { c as pluginCacheLstatSync, d as readPluginCacheDirectory, l as pluginCacheRealpathSync, s as pluginCacheExistsSync } from "./package-manifest-DKd3Hflg.mjs"; import { c as readRootJsonObjectSync } from "./json-files-C5mNTScg.mjs"; import { n as registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle-COmFOHdR.mjs"; import { t as shouldRejectHardlinkedPluginFiles } from "./hardlink-policy-CsRTs3Ca.mjs"; import { u as resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot-DZdnLZdU.mjs"; import "./skill-library-Bgib5R_8.mjs"; import { i as resolveSkillManifestMetadata, n as resolveSkillInvocationPolicy } from "./frontmatter-JGnV9USz.mjs"; import { r as resolveSkillTelemetrySourceValue } from "./source-DbWOnpPI.mjs"; import { t as SKILL_LIBRARY_MAX_TREE_ENTRIES } from "./bundle-LTZCg5yw.mjs"; import { t as isAcpRuntimeSpawnAvailable } from "./availability-DCCVp-oz.mjs"; import { r as resolveWorkspaceSkillDirectories, t as normalizeWorkspaceSkillRoots } from "./workspace-skill-roots-CL28VeX5.mjs"; import { n as getSkillRootDiscoveryEpoch, o as markSkillsSupportingFilesChanged, p as suspendSkillsSnapshotSources, s as notifySkillsWatchAvailable, t as bumpSkillsSnapshotVersion } from "./refresh-state-CJGdcI_j.mjs"; import { c as tryRealpath, i as resolvePluginSkillsDir, r as compactSkillPath, s as resolveAllowedSkillSymlinkTargetRealPaths, t as loadSingleSkillDirectory } from "./local-loader-DBsWVCHZ.mjs"; import { a as resolveSkillDiscoveryLimits, n as discoverPluginSkills, r as discoverSkillCandidates, t as canonicalSkillDirForSource } from "./skill-root-discovery-B5Jg-4aX.mjs"; import { t as iteratePluginRootContributions } from "./plugin-root-contributions-BDEvLZm6.mjs"; import fs from "node:fs"; import path from "node:path"; import { TextDecoder } from "node:util"; const SKILL_SOURCE_ORIGIN_RELATIVE_PATH = `.openclaw/source-origin.json`; //#endregion //#region src/skills/loading/skill-entry-metadata.ts const MAX_SKILL_SOURCE_ORIGIN_BYTES = 16384; function readSourceInstallSkillKey(skillDir) { try { const sourceOriginPath = path.join(skillDir, SKILL_SOURCE_ORIGIN_RELATIVE_PATH); const parentRealPath = tryRealpath(path.dirname(sourceOriginPath)); if (!parentRealPath) return; const skillDirRealPath = tryRealpath(skillDir); if (!skillDirRealPath) return; const result = readRootJsonObjectSync({ rootDir: skillDirRealPath, rootRealPath: skillDirRealPath, relativePath: path.relative(skillDirRealPath, path.join(parentRealPath, path.basename(sourceOriginPath))), boundaryLabel: "skill directory", rejectHardlinks: false, maxBytes: MAX_SKILL_SOURCE_ORIGIN_BYTES }); return result.ok ? normalizeOptionalString(result.value.slug) : void 0; } catch { return; } } function createSkillEntry(record) { const { skill, frontmatter } = record; const invocation = resolveSkillInvocationPolicy(frontmatter); let metadata = resolveSkillManifestMetadata(frontmatter); if (!metadata?.skillKey) { const skillKey = readSourceInstallSkillKey(skill.baseDir); if (skillKey) metadata = { ...metadata, skillKey }; } return { ...record.sourceOrder !== void 0 ? { sourceOrder: record.sourceOrder } : {}, skill, frontmatter, metadata, invocation, exposure: { includeInRuntimeRegistry: true, includeInAvailableSkillsPrompt: !invocation.disableModelInvocation, userInvocable: invocation.userInvocable ?? true }, ...record.syncSourceDir !== void 0 ? { syncSourceDir: record.syncSourceDir } : {}, ...record.syncDirName !== void 0 ? { syncDirName: record.syncDirName } : {} }; } //#endregion //#region src/skills/runtime/refresh-watch-path.ts function isTrustedSymlinkSkillTarget(source, rootRealPath, targetRealPath, allowedSymlinkTargetRealPaths) { if (source === "openclaw-managed" || source === "agents-skills-personal") return true; return isPathInside$1(rootRealPath, targetRealPath) || allowedSymlinkTargetRealPaths.some((root) => isPathInside$1(root, targetRealPath)); } function toWatchRoot(raw) { const normalized = raw.replaceAll("\\", "/"); const root = path.parse(normalized).root; const trimmed = normalized.replace(/\/+$/, ""); return trimmed.length < root.length ? root : trimmed; } const DEFAULT_SKILLS_WATCH_IGNORED = [ /(^|[\\/])\.git([\\/]|$)/, /(^|[\\/])node_modules([\\/]|$)/, /(^|[\\/])dist([\\/]|$)/, /(^|[\\/])\.venv([\\/]|$)/, /(^|[\\/])venv([\\/]|$)/, /(^|[\\/])__pycache__([\\/]|$)/, /(^|[\\/])\.mypy_cache([\\/]|$)/, /(^|[\\/])\.pytest_cache([\\/]|$)/, /(^|[\\/])build([\\/]|$)/, /(^|[\\/])\.cache([\\/]|$)/, /(^|[\\/])\.archive([\\/]|$)/ ]; const isIgnoredSkillsWatchPath = (candidate) => DEFAULT_SKILLS_WATCH_IGNORED.some((pattern) => pattern.test(candidate)); function isSkillDiscoveryFileWatchPath(watchPath) { const basename = path.posix.basename(watchPath.replaceAll("\\", "/")); return (basename === "SKILL.md" || basename === "source-origin.json") && !DEFAULT_SKILLS_WATCH_IGNORED.some((re) => re.test(watchPath)); } function readBudgetedDirEntries(dir, maxEntries) { const entries = []; const limit = Math.max(0, maxEntries); let handle; try { handle = fs.opendirSync(dir); for (let scanned = 0; scanned < limit; scanned += 1) { const entry = handle.readSync(); if (!entry) return { ok: true, entries, scannedEntryCount: scanned }; entries.push(entry); } return { ok: true, entries, scannedEntryCount: limit }; } catch { return { ok: false, scannedEntryCount: 0 }; } finally { handle?.closeSync(); } } //#endregion //#region src/skills/runtime/refresh-watch-registry.ts const pathWatchers = /* @__PURE__ */ new Map(); const workspaceWatchTargets = /* @__PURE__ */ new Map(); let watchGeneration = 0; let rootTargetPaths; const replanningRoots = /* @__PURE__ */ new Map(); function nextSkillsWatchGeneration() { return ++watchGeneration; } function setWorkspaceWatchTargets(watcherKey, targets) { workspaceWatchTargets.set(watcherKey, targets); rootTargetPaths = void 0; replanningRoots.delete(watcherKey); } /** A resolved plan, changed or not, settles target discovery for this owner. */ function settleWorkspaceWatchTargetsPlan(watcherKey, resolved) { workspaceWatchTargetCache.set(watcherKey, resolved); replanningRoots.delete(watcherKey); } function clearWorkspaceWatchTargets() { workspaceWatchTargets.clear(); rootTargetPaths = void 0; replanningRoots.clear(); } function isVerifiedWatch(state) { return Boolean(state && !state.closed && !state.failed && state.verified && !state.unavailable && state.initialScan === "ready"); } /** Verified watch coverage for one configured root, keyed by its targets' generations. */ function readSkillRootDiscoveryToken(rootDir) { if (!rootTargetPaths) { const pathsByRoot = /* @__PURE__ */ new Map(); for (const targets of workspaceWatchTargets.values()) for (const target of targets) for (const root of target.roots) { const paths = pathsByRoot.get(root) ?? /* @__PURE__ */ new Set(); paths.add(target.path); pathsByRoot.set(root, paths); } rootTargetPaths = new Map([...pathsByRoot].map(([root, paths]) => [root, [...paths].toSorted()])); } const root = path.resolve(rootDir); const paths = rootTargetPaths.get(root); if (!paths?.length) return; for (const roots of replanningRoots.values()) if (roots.has(root)) return; const pairs = []; for (const targetPath of paths) { const state = pathWatchers.get(targetPath); if (!isVerifiedWatch(state)) return; pairs.push(`${targetPath}:${state.depth}:${state.generation}`); } return JSON.stringify(pairs); } /** The verified watch target that observes a path within its depth and exclusions. */ function observeSkillDiscoveryPath(filePath) { let observed; for (const [targetPath, state] of pathWatchers) { if (!isVerifiedWatch(state) || !isPathInside$1(targetPath, filePath)) continue; const relative = path.relative(targetPath, filePath); if (isIgnoredSkillsWatchPath(relative) || relative.split(path.sep).filter(Boolean).length > state.depth) continue; if (!observed || targetPath.length > observed.target.length) observed = { target: targetPath, generation: state.generation }; } return observed; } function isSkillDiscoveryDependencyCurrent(dependency) { const state = pathWatchers.get(dependency.target); return isVerifiedWatch(state) && state.generation === dependency.generation; } const workspaceWatchOwners = /* @__PURE__ */ new Map(); const workspaceWatchTargetCache = /* @__PURE__ */ new Map(); function unsubscribeWorkspaceFromPath(workspaceDir, watchTarget) { const state = pathWatchers.get(watchTarget.path); if (!state) return; state.subscribers.delete(workspaceDir); if (state.subscribers.size === 0) state.close().then(() => { if (state.subscribers.size === 0 && pathWatchers.get(watchTarget.path) === state) pathWatchers.delete(watchTarget.path); }, () => { state.failed = true; }); } const workspaceWatchLastEnsuredAt = /* @__PURE__ */ new Map(); const detachedWatchers = []; function releaseSkillsWatchers(mode) { const active = [...detachedWatchers.splice(0), ...pathWatchers.values()]; pathWatchers.clear(); clearWorkspaceWatchTargets(); workspaceWatchOwners.clear(); workspaceWatchTargetCache.clear(); workspaceWatchLastEnsuredAt.clear(); for (const state of active) if (mode === "detach") { state.closed = true; state.verified = false; clearTimeout(state.timer); detachedWatchers.push(state); } else state.close(); } function disposeWorkspacePathWatchState(watcherKey, targets) { for (const target of targets) unsubscribeWorkspaceFromPath(watcherKey, target); workspaceWatchTargets.delete(watcherKey); rootTargetPaths = void 0; replanningRoots.delete(watcherKey); workspaceWatchOwners.delete(watcherKey); workspaceWatchTargetCache.delete(watcherKey); workspaceWatchLastEnsuredAt.delete(watcherKey); } const SKILLS_WORKSPACE_WATCH_IDLE_TTL_MS = 36e5; const MAX_SKILLS_WORKSPACE_WATCH_STATES = 128; function evictWorkspaceWatchStates(now, dispose) { const evict = (watcherKey) => { const owner = workspaceWatchOwners.get(watcherKey); dispose(watcherKey); if (!owner) return; const remainingOwners = Array.from(workspaceWatchOwners.values()).filter((other) => other.workspaceDir === owner.workspaceDir); if (remainingOwners.length === 0) suspendSkillsSnapshotSources(owner.workspaceDir, {}); if (owner.sourceScope.executionWorkspaceDir && !remainingOwners.some((other) => other.sourceScope.executionWorkspaceDir === owner.sourceScope.executionWorkspaceDir && other.sourceScope.executionWorkspaceFileHost === owner.sourceScope.executionWorkspaceFileHost)) suspendSkillsSnapshotSources(owner.workspaceDir, owner.sourceScope); }; const cutoff = now - SKILLS_WORKSPACE_WATCH_IDLE_TTL_MS; for (const [watcherKey, lastEnsuredAt] of workspaceWatchLastEnsuredAt) if (lastEnsuredAt < cutoff) evict(watcherKey); for (const watcherKey of workspaceWatchLastEnsuredAt.keys()) { if (workspaceWatchLastEnsuredAt.size <= MAX_SKILLS_WORKSPACE_WATCH_STATES) break; evict(watcherKey); } } const hasUnreadySharedTargets = (watcherKey) => (workspaceWatchTargets.get(watcherKey) ?? []).some((target) => !target.executionOnly && pathWatchers.get(target.path)?.initialScan !== "ready"); function hasVerifiedCoverage(watcherKey) { return workspaceWatchTargets.get(watcherKey)?.every((target) => { const state = pathWatchers.get(target.path); return state && !state.closed && state.verified && !state.unavailable; }) ?? false; } function publishRecoveredCoverage() { for (const [watcherKey, owner] of workspaceWatchOwners) if (owner.unavailable && hasVerifiedCoverage(watcherKey)) { owner.unavailable = false; notifySkillsWatchAvailable({ workspaceDir: owner.workspaceDir, sourceScope: owner.sourceScope }); } } function publishSkillsWatchChanges(changes) { const affected = /* @__PURE__ */ new Map(); for (const pending of changes) for (const watcherKey of pending.watcherKeys) { const owner = workspaceWatchOwners.get(watcherKey); const targets = workspaceWatchTargets.get(watcherKey); if (owner && targets) { const entries = affected.get(owner.workspaceDir) ?? []; entries.push({ pending, watcherKey, targets }); affected.set(owner.workspaceDir, entries); } } for (const [workspaceDir, entries] of affected) { const scopes = /* @__PURE__ */ new Map(); let changedPath; let unavailable = false; for (const { pending, watcherKey, targets } of entries) { const owner = workspaceWatchOwners.get(watcherKey); const { targetPath, state, change } = pending; if (!owner || state.closed || pathWatchers.get(targetPath) !== state || !state.subscribers.has(watcherKey) || workspaceWatchTargets.get(watcherKey) !== targets) continue; if (change !== "supporting") { workspaceWatchTargetCache.delete(watcherKey); changedPath = pending.changedPath; } if (change === "skills") { const roots = replanningRoots.get(watcherKey) ?? /* @__PURE__ */ new Set(); for (const root of targets.find((target) => target.path === targetPath)?.roots ?? []) roots.add(root); replanningRoots.set(watcherKey, roots); } unavailable ||= change === "unavailable"; owner.unavailable ||= change === "unavailable"; const initialScan = change === "initial-scan"; const shared = initialScan ? owner.sharedScanPending : targets.find((entry) => entry.path === targetPath)?.executionOnly !== true; if (initialScan && shared) for (const [key, other] of workspaceWatchOwners) other.sharedScanPending &&= other.workspaceDir !== workspaceDir || hasUnreadySharedTargets(key); const kind = change === "supporting" ? "supporting" : "skills"; if (shared) scopes.set(kind, void 0); else if (!scopes.has(kind) || scopes.get(kind)) { const selected = scopes.get(kind) ?? []; if (!selected.some((scope) => scope.executionWorkspaceDir === owner.sourceScope.executionWorkspaceDir && scope.executionWorkspaceFileHost === owner.sourceScope.executionWorkspaceFileHost)) selected.push(owner.sourceScope); scopes.set(kind, selected); } } if (scopes.has("supporting")) markSkillsSupportingFilesChanged({ workspaceDir, sourceScopes: scopes.get("supporting") }); if (scopes.has("skills")) bumpSkillsSnapshotVersion({ workspaceDir, sourceScopes: scopes.get("skills"), reason: unavailable ? "watch-unavailable" : "watch", changedPath }); } drainSkillsWatchReplans(); } let drainingReplans = false; function drainSkillsWatchReplans() { if (drainingReplans) return; drainingReplans = true; try { for (const watcherKey of replanningRoots.keys()) workspaceWatchOwners.get(watcherKey)?.reconcileTargets?.(); } finally { drainingReplans = false; } } function flushSkillsWatchChanges(trigger) { if (trigger.closed) return; const now = performance.now(); const changes = []; for (const [targetPath, state] of pathWatchers) { if (state.closed || state.timer === void 0 || state !== trigger && (state.pendingAt === void 0 || state.pendingAt > now)) continue; changes.push({ targetPath, state, watcherKeys: state.subscribers, changedPath: state.pendingPath, change: state.pendingChange ?? "skills" }); clearTimeout(state.timer); state.timer = void 0; state.pendingAt = void 0; state.pendingPath = void 0; state.pendingChange = void 0; } publishSkillsWatchChanges(changes); } /** Discovery host is part of watcher identity, including identical path strings. */ function resolveSkillsWatchScope(params) { const workspaceDir = params.workspaceDir.trim(); const { executionWorkspaceDir, executionWorkspaceFileHost } = normalizeWorkspaceSkillRoots({ agentWorkspaceDir: workspaceDir, executionWorkspaceDir: params.executionWorkspaceDir, executionWorkspaceFileHost: params.executionWorkspaceFileHost }); return { workspaceDir, executionWorkspaceDir, watcherKey: JSON.stringify([ workspaceDir, executionWorkspaceDir, params.agentId, executionWorkspaceFileHost ]), sourceScope: { executionWorkspaceDir, executionWorkspaceFileHost } }; } //#endregion //#region src/skills/loading/skill-root-loader.ts const skillsLogger = createSubsystemLogger("skills"); const skillRootRecordsCache = /* @__PURE__ */ new Map(); const MAX_DISCOVERY_LINK_HOPS = 40; function resolveRecordingLinks(input, links, budget = { hops: MAX_DISCOVERY_LINK_HOPS }) { const absolute = path.resolve(input); let current = path.parse(absolute).root; const parts = absolute.slice(current.length).split(path.sep).filter(Boolean); for (const [index, part] of parts.entries()) { const next = path.join(current, part); try { if (!fs.lstatSync(next).isSymbolicLink()) { current = next; continue; } budget.hops -= 1; links.add(next); const resolved = budget.hops >= 0 ? resolveRecordingLinks(path.resolve(path.dirname(next), fs.readlinkSync(next)), links, budget) : void 0; if (!resolved) return; current = resolved; } catch (error) { return isMissingPathError(error) ? path.join(current, ...parts.slice(index)) : void 0; } } return current; } /** * Watchers that observe every link discovery followed and every directory it * inspected. Undefined when any of them is unobserved, so the scan is not reused. */ function collectDiscoveryDependencies(inspectedPaths, linkOnlyPaths) { const links = /* @__PURE__ */ new Set(); const observedPaths = []; for (const entry of inspectedPaths) { const realPath = resolveRecordingLinks(entry, links); if (!realPath) return; observedPaths.push(realPath); } for (const entry of linkOnlyPaths) if (!resolveRecordingLinks(entry, links)) return; const dependencies = /* @__PURE__ */ new Map(); for (const observedPath of [...links, ...observedPaths]) { const dependency = observeSkillDiscoveryPath(observedPath); if (!dependency) return; dependencies.set(dependency.target, dependency); } return [...dependencies.values()]; } function hasAliasedDiscoveryFile(skillDir) { for (const relative of [ "SKILL.md", ".openclaw", SKILL_SOURCE_ORIGIN_RELATIVE_PATH ]) try { const stat = fs.lstatSync(path.join(skillDir, relative)); if (stat.isSymbolicLink() || stat.isFile() && stat.nlink > 1) return true; } catch (error) { if (!isMissingPathError(error)) return true; } return false; } function clearSkillRootRecordsCache() { skillRootRecordsCache.clear(); } function warnInvalidSkill(source, diagnostic) { skillsLogger.warn("Skipping invalid skill.", { source, filePath: diagnostic.path, error: diagnostic.message, consoleMessage: `Skipping invalid skill: file=${compactSkillPath(diagnostic.path)} error=${diagnostic.message}` }); } function loadContainedSkillRecord(params) { const loaded = loadSingleSkillDirectory({ ...params, onDiagnostic: params.onDiagnostic ?? ((diagnostic) => warnInvalidSkill(params.source, diagnostic)) }); if (!loaded) return null; const record = { skill: loaded.skill, frontmatter: loaded.frontmatter }; if (!params.canonicalSkillDir) return record; const originalBaseDir = path.resolve(record.skill.baseDir); const canonicalBaseDir = path.resolve(params.canonicalSkillDir); if (originalBaseDir === canonicalBaseDir) return record; const filePath = path.join(canonicalBaseDir, path.relative(originalBaseDir, record.skill.filePath)); return { ...record, syncSourceDir: canonicalBaseDir, syncDirName: path.basename(originalBaseDir), skill: { ...record.skill, filePath, baseDir: canonicalBaseDir, sourceInfo: record.skill.sourceInfo ? { ...record.skill.sourceInfo, path: filePath, baseDir: canonicalBaseDir } : record.skill.sourceInfo } }; } /** Loads one skill root under the configured discovery limits and symlink/hardlink policy. */ function loadSkillRootRecords(params) { const discoveryRoot = { path: path.resolve(params.dir), worktree: params.worktree ?? isPathInside$1(params.config?.worktreeRoot ?? path.join(resolveStateDir(), "worktrees"), params.dir) }; const limits = resolveSkillDiscoveryLimits(params.config); if (params.mode === "audit") { const defaults = resolveSkillDiscoveryLimits(); limits.maxCandidatesPerRoot = Math.max(limits.maxCandidatesPerRoot, defaults.maxCandidatesPerRoot); limits.maxSkillsLoadedPerSource = Math.max(limits.maxSkillsLoadedPerSource, defaults.maxSkillsLoadedPerSource); limits.maxSkillFileBytes = defaults.maxSkillFileBytes; } const rejectHardlinks = params.rejectHardlinks ?? shouldRejectHardlinkedPluginFiles({ origin: resolveSkillTelemetrySourceValue(params.source) === "bundled" ? "bundled" : "workspace", rootDir: params.dir }); const allowedSymlinkTargetRealPaths = resolveAllowedSkillSymlinkTargetRealPaths(params.config); const token = params.mode === "audit" || params.onDiagnostic ? void 0 : readSkillRootDiscoveryToken(params.dir); const cacheSlot = JSON.stringify([discoveryRoot.path, params.source]); const cacheKey = token === void 0 ? void 0 : JSON.stringify([ discoveryRoot.path, params.source, discoveryRoot.worktree, limits, allowedSymlinkTargetRealPaths, rejectHardlinks, getSkillRootDiscoveryEpoch(), token ]); const cached = skillRootRecordsCache.get(cacheSlot); if (cacheKey !== void 0 && cached?.key === cacheKey && cached.dependencies.every(isSkillDiscoveryDependencyCurrent)) return cached.records.slice(); let unresolved = false; const linkOnlyPaths = []; const inspectedDirs = /* @__PURE__ */ new Set([discoveryRoot.path]); const discovered = discoverSkillCandidates({ dir: params.dir, source: params.source, limits, allowedSymlinkTargetRealPaths, onDiagnostic: (diagnostic) => { unresolved ||= diagnostic.kind === "read"; if (diagnostic.kind === "invalid") linkOnlyPaths.push(diagnostic.path); params.onDiagnostic?.(diagnostic); }, onDirectory: (dir) => inspectedDirs.add(path.resolve(dir)), onSymlink: (link) => linkOnlyPaths.push(link) }); const dependencies = cacheKey === void 0 || unresolved || [...discovered.candidates, discovered.configuredRootCandidate].some((candidate) => candidate && hasAliasedDiscoveryFile(candidate.skillDir)) ? void 0 : collectDiscoveryDependencies([...inspectedDirs], linkOnlyPaths); const remember = (records) => { if (cacheKey !== void 0 && dependencies) { skillRootRecordsCache.set(cacheSlot, { key: cacheKey, dependencies, records }); pruneMapToMaxSize(skillRootRecordsCache, 256); } return records.slice(); }; const maxSkillsLoadedPerSource = Math.max(0, limits.maxSkillsLoadedPerSource); const loadCandidate = (candidate) => { const record = loadContainedSkillRecord({ skillDir: candidate.skillDir, rootRealPath: candidate.skillDirRealPath, source: params.source, maxBytes: limits.maxSkillFileBytes, canonicalSkillDir: params.mode === "audit" ? candidate.skillDirRealPath : canonicalSkillDirForSource(params.source, candidate.skillDirRealPath), rejectHardlinks, onDiagnostic: params.onDiagnostic }); if (record) record.skill.discoveryRoot = discoveryRoot; return record; }; if (discovered.configuredRootCandidate) { const rootRecord = loadCandidate(discovered.configuredRootCandidate); if (rootRecord) return remember([rootRecord]); } const loadedSkills = []; for (const candidate of discovered.candidates) { if (params.mode !== "audit" && !discovered.rootIsSkill && loadedSkills.length >= maxSkillsLoadedPerSource) break; const record = loadCandidate(candidate); if (record) loadedSkills.push(record); } return remember(loadedSkills); } function loadGeneratedPluginSkillRecords(params) { const candidates = discoverPluginSkills(params); const maxSkillsLoadedPerSource = Math.max(0, params.limits.maxSkillsLoadedPerSource); const loadedSkills = []; for (const candidate of candidates) { const record = loadContainedSkillRecord({ skillDir: candidate.skillDir, rootRealPath: candidate.skillDirRealPath, source: params.source, maxBytes: params.limits.maxSkillFileBytes, rejectHardlinks: candidate.rejectHardlinks }); if (record) { record.skill.discoveryRoot = { path: path.resolve(params.pluginSkillsDir), worktree: false }; loadedSkills.push({ ...record, syncSourceDir: candidate.skillDirRealPath, syncDirName: path.basename(record.skill.baseDir) }); } if (loadedSkills.length >= maxSkillsLoadedPerSource) break; } return loadedSkills; } /** Scan selected roots on their owning host, retaining native precedence and file rules. */ function loadWorkspaceSkillSourceEntries(plan, config) { const grouped = /* @__PURE__ */ new Map(); for (const root of plan.roots) { const records = grouped.get(root.tier) ?? []; for (const record of loadSkillRootRecords({ ...root, config })) records.push({ ...record, sourceOrder: root.order }); grouped.set(root.tier, records); } const extra = grouped.get("extra") ?? []; if (plan.pluginSkillsDir) for (const record of loadGeneratedPluginSkillRecords({ pluginSkillsDir: plan.pluginSkillsDir, pluginSkillRoots: plan.pluginSkillRoots, source: "openclaw-extra", limits: resolveSkillDiscoveryLimits(config) })) extra.push({ ...record, sourceOrder: (plan.roots.find((root) => root.tier !== "extra")?.order ?? Math.max(-1, ...plan.roots.map((root) => root.order ?? -1)) + 1) - .5 }); grouped.set("extra", extra); grouped.get("bundled")?.sort((left, right) => left.skill.name.localeCompare(right.skill.name, "en") || left.skill.source.localeCompare(right.skill.source, "en")); return [ "extra", "bundled", "workshop", "managed", "personal", "workspace" ].flatMap((tier) => grouped.get(tier) ?? []).map(createSkillEntry); } function loadExecutionSkillEntries(executionWorkspaceDir, config) { return resolveWorkspaceSkillDirectories(executionWorkspaceDir).flatMap((root) => loadSkillRootRecords({ ...root, config })).map(createSkillEntry); } //#endregion //#region src/skills/loading/plugin-skill-bundle.ts /** Portable relative paths keep catalog paths and installed reads on the same boundary. */ function validatePluginSkillPath(value) { const parts = value.split("/"); if (!value || value.length > 512 || parts.length > 16 || parts.some((part) => !part || part === "." || part === "..") || containsAsciiControlCharacter(value) || /[\\:]/u.test(value)) throw new Error("Invalid plugin skill bundle path."); } function pluginSkillFileFromBytes(filePath, buffer) { try { const content = new TextDecoder("utf-8", { fatal: true }).decode(buffer); if (buffer.some((byte) => byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) throw new Error("Binary content"); return { path: filePath, sizeBytes: buffer.length, status: "ready", content }; } catch { return { path: filePath, sizeBytes: buffer.length, status: "binary" }; } } /** Inventory and reads share one pinned plugin root; links are represented but never followed. */ async function readPluginSkillBundle(params) { if (params.rootPath !== ".") validatePluginSkillPath(params.rootPath); const selectedPath = params.path ?? "SKILL.md"; validatePluginSkillPath(selectedPath); const owner = await root(params.pluginRoot); const result = { name: params.name, rootPath: params.rootPath, entryPath: "SKILL.md", files: [], directories: [], inventoryComplete: true }; let entries = 0; let bytes = 0; async function visit(directory, depth) { if (depth > 16) throw new Error("Skill bundle exceeds directory depth limit."); const relative = directory ? `${params.rootPath}/${directory}` : params.rootPath; for await (const entry of owner.entries(relative, { order: "sorted", maxEntries: SKILL_LIBRARY_MAX_TREE_ENTRIES - entries, symlinks: "reject" })) { if (++entries > SKILL_LIBRARY_MAX_TREE_ENTRIES) throw new Error("Skill bundle exceeds inventory limits."); const filePath = directory ? `${directory}/${entry.name}` : entry.name; validatePluginSkillPath(filePath); if (entry.isDirectory && !entry.isSymbolicLink) { result.directories.push(filePath); await visit(filePath, depth + 1); continue; } if (result.files.length >= 256) throw new Error("Skill bundle exceeds file count limit."); const file = { path: filePath, sizeBytes: entry.size, status: "unavailable" }; result.files.push(file); if (!entry.isFile || entry.isSymbolicLink) continue; if (entry.size > 1048576 || bytes + entry.size > 8388608) { file.status = "too-large"; continue; } bytes += entry.size; file.status = "deferred"; } } await visit("", 0); if (params.path !== void 0 && !result.files.some((file) => file.path === selectedPath)) throw new Error("Plugin skill file not found."); result.files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); const selected = result.files.find((file) => file.path === selectedPath); if (selected?.status === "deferred") { selected.status = "unavailable"; try { const read = await owner.read(`${params.rootPath}/${selectedPath}`, { symlinks: "reject", hardlinks: params.rejectHardlinks ? "reject" : "allow", maxBytes: selected.sizeBytes }); Object.assign(selected, pluginSkillFileFromBytes(selectedPath, read.buffer)); } catch (error) { if (error instanceof FsSafeError && error.code === "too-large") selected.status = "too-large"; } } return result; } //#endregion //#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 || params.config?.plugins?.enabled === false) { publishPluginSkills([], 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) { const workspaceDir = (params.workspaceDir ?? "").trim(); const metadataSnapshot = params.metadataSnapshot; if (!workspaceDir || metadataSnapshot.manifestRegistry.plugins.length === 0) { publishPluginSkills([], params.pluginSkillsDir); return []; } const config = params.config ?? {}; const acpRuntimeAvailable = isAcpRuntimeSpawnAvailable({ config }); const seen = /* @__PURE__ */ new Set(); const resolved = []; for (const { record, roots } of iteratePluginRootContributions({ metadataSnapshot, config, contribution: "skills", isAvailable: (candidate) => acpRuntimeAvailable || candidate.id !== "acpx" })) { const rejectHardlinks = shouldRejectHardlinkedPluginFiles({ origin: record.origin, rootDir: record.rootDir }); for (const raw of roots) { 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), 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)); } /** Resolve manifest skill roots to the names users and agents actually see. */ function resolvePluginSkillRecords(record) { const rejectHardlinks = shouldRejectHardlinkedPluginFiles({ origin: record.origin, rootDir: record.rootDir }); const records = /* @__PURE__ */ new Map(); for (const raw of record.skills) { const candidate = path.resolve(record.rootDir, raw.trim()); if (!raw.trim() || !pluginCacheExistsSync(candidate)) continue; if (!isPluginSkillPathInside(record.rootDir, candidate)) { log.warn(`plugin skill path escapes plugin root (${record.id}): ${candidate}`); continue; } for (const loaded of loadSkillRootRecords({ dir: candidate, source: record.origin === "bundled" ? "bundled" : "plugin", mode: "audit", rejectHardlinks })) records.set(loaded.skill.filePath, loaded); } return [...records.values()]; } function resolvePluginSkillDetails(record) { return [...new Map(resolvePluginSkillRecords(record).map(({ skill }) => [skill.name, { name: skill.name, ...skill.description ? { description: skill.description } : {} }])).values()].toSorted((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); } async function readPluginSkill(record, name, selection) { if (selection?.version !== void 0 && selection.version !== record.version) throw new Error("Installed plugin version changed. Reopen the skill preview."); const matches = resolvePluginSkillRecords(record).filter(({ skill }) => skill.name === name); if (matches.length !== 1) throw new Error(matches.length ? "Plugin skill name is ambiguous." : "Plugin skill not found."); const pluginRoot = pluginCacheRealpathSync(record.rootDir); if (!pluginRoot) throw new Error("Installed plugin root not found."); return { ...await readPluginSkillBundle({ pluginRoot, rootPath: path.relative(pluginRoot, matches[0].skill.baseDir).split(path.sep).join("/") || ".", name, rejectHardlinks: shouldRejectHardlinkedPluginFiles(record), path: selection?.path }), ...record.version ? { version: record.version } : {} }; } 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 collectSkillTargets(dir, targets) { const candidates = hasPublishableSkillFile({ skillDir: dir, rootDir: dir }) ? [{ name: path.basename(dir), path: dir }] : listSkillChildDirectories(dir).filter((entry) => hasPublishableSkillFile({ skillDir: entry.path, rootDir: dir })); for (const entry of candidates) { const existing = targets.get(entry.name); if (existing) { log.warn(`plugin skill name collision: "${entry.name}" resolves to both ${existing} and ${entry.path}; only the first will be published`); continue; } targets.set(entry.name, entry.path); } } 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, directory) { const pluginSkillsDir = directory ?? resolvePluginSkillsDir(); const managedTargets = /* @__PURE__ */ new Map(); for (const dir of skillDirs) collectSkillTargets(dir, managedTargets); if (directory === void 0 && lastDefaultPluginSkillsPublication?.directory === pluginSkillsDir && lastDefaultPluginSkillsPublication.targets.size === managedTargets.size && [...managedTargets].every(([name, target]) => lastDefaultPluginSkillsPublication?.targets.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, process.platform === "win32" ? "junction" : "dir"); } 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 (directory === void 0) lastDefaultPluginSkillsPublication = { directory: pluginSkillsDir, targets: 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 { DEFAULT_SKILLS_WATCH_IGNORED as A, setWorkspaceWatchTargets as C, workspaceWatchOwners as D, workspaceWatchLastEnsuredAt as E, toWatchRoot as F, createSkillEntry as I, isSkillDiscoveryFileWatchPath as M, isTrustedSymlinkSkillTarget as N, workspaceWatchTargetCache as O, readBudgetedDirEntries as P, resolveSkillsWatchScope as S, unsubscribeWorkspaceFromPath as T, nextSkillsWatchGeneration as _, pluginSkillFileFromBytes as a, publishSkillsWatchChanges as b, loadExecutionSkillEntries as c, warnInvalidSkill as d, disposeWorkspacePathWatchState as f, hasVerifiedCoverage as g, hasUnreadySharedTargets as h, resolvePluginSkillRootsFromMetadata as i, isIgnoredSkillsWatchPath as j, workspaceWatchTargets as k, loadSkillRootRecords as l, flushSkillsWatchChanges as m, resolvePluginSkillDetails as n, validatePluginSkillPath as o, evictWorkspaceWatchStates as p, resolvePluginSkillRoots as r, clearSkillRootRecordsCache as s, readPluginSkill as t, loadWorkspaceSkillSourceEntries as u, pathWatchers as v, settleWorkspaceWatchTargetsPlan as w, releaseSkillsWatchers as x, publishRecoveredCoverage as y };