From efffb42ef9fd991cb5d7cecf06859e6769628b77 Mon Sep 17 00:00:00 2001 From: Shakker Date: Fri, 29 May 2026 15:29:20 +0100 Subject: [PATCH] refactor: split skills index follow-up --- src/agents/command/attempt-execution.ts | 2 +- src/agents/sessions/skills.ts | 1 - src/auto-reply/reply/session-updates.test.ts | 2 +- src/commands/agent-command.test-mocks.ts | 5 +- src/skills/discovery/registry.ts | 94 ------ src/skills/discovery/service.test.ts | 297 ------------------- src/skills/discovery/service.ts | 253 ---------------- src/skills/discovery/trust.ts | 107 ------- src/skills/index.ts | 65 ---- src/skills/runtime/session-snapshot.ts | 2 +- 10 files changed, 4 insertions(+), 824 deletions(-) delete mode 100644 src/agents/sessions/skills.ts delete mode 100644 src/skills/discovery/registry.ts delete mode 100644 src/skills/discovery/service.test.ts delete mode 100644 src/skills/discovery/service.ts delete mode 100644 src/skills/discovery/trust.ts delete mode 100644 src/skills/index.ts diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index e87c887428b..398aa1fb3cf 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -19,7 +19,7 @@ import { appendUserTurnTranscriptMessage, type PersistedUserTurnMessage, } from "../../sessions/user-turn-transcript.js"; -import { buildWorkspaceSkillSnapshot } from "../../skills/discovery/service.js"; +import { buildWorkspaceSkillSnapshot } from "../../skills/loading/workspace.js"; import { sanitizeForLog } from "../../terminal/ansi.js"; import { resolveMessageChannel } from "../../utils/message-channel.js"; import { resolveAuthProfileOrder } from "../auth-profiles/order.js"; diff --git a/src/agents/sessions/skills.ts b/src/agents/sessions/skills.ts deleted file mode 100644 index 6ff1a885a6f..00000000000 --- a/src/agents/sessions/skills.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../skills/loading/session.js"; diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts index 43ad2fa4b52..243e0c43222 100644 --- a/src/auto-reply/reply/session-updates.test.ts +++ b/src/auto-reply/reply/session-updates.test.ts @@ -57,7 +57,7 @@ vi.mock("../../skills/runtime/remote.js", () => ({ getRemoteSkillEligibility: getRemoteSkillEligibilityMock, })); -vi.mock("../../skills/discovery/service.js", () => ({ +vi.mock("../../skills/loading/workspace.js", () => ({ buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock, })); diff --git a/src/commands/agent-command.test-mocks.ts b/src/commands/agent-command.test-mocks.ts index 3ff8d0cd26b..f5cd957ce88 100644 --- a/src/commands/agent-command.test-mocks.ts +++ b/src/commands/agent-command.test-mocks.ts @@ -239,11 +239,8 @@ vi.mock("../agents/workspace.js", () => ({ ensureAgentWorkspace: vi.fn(async ({ dir }: { dir: string }) => ({ dir })), })); -vi.mock("../skills/discovery/service.js", () => ({ - buildWorkspaceSkillSnapshot: vi.fn(() => undefined), -})); - vi.mock("../skills/loading/workspace.js", () => ({ + buildWorkspaceSkillSnapshot: vi.fn(() => undefined), loadWorkspaceSkillEntries: vi.fn(() => []), })); diff --git a/src/skills/discovery/registry.ts b/src/skills/discovery/registry.ts deleted file mode 100644 index ad15dca71ff..00000000000 --- a/src/skills/discovery/registry.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Skill } from "../loading/skill-contract.js"; -import type { SkillEntry } from "../types.js"; -import { resolveSkillTrustInfo, type SkillSourceKind } from "./trust.js"; - -export type SkillIndexEntry = { - id: string; - name: string; - description: string; - path: string; - baseDir: string; - sourceLabel: string; - sourceKind: SkillSourceKind; - owner: string; - writable: boolean; - writableReason: string; - entry: SkillEntry; -}; - -export type SkillIndex = { - cacheKey: string; - builtAt: number; - entries: SkillIndexEntry[]; - byId: ReadonlyMap; - byName: ReadonlyMap; - byPath: ReadonlyMap; -}; - -export function createSkillId(params: { - sourceKind: SkillSourceKind; - sourceLabel: string; - name: string; - path: string; -}): string { - return `${params.sourceKind}:${params.sourceLabel}:${params.name}:${params.path}`; -} - -export function buildSkillIndex(params: { - cacheKey: string; - entries: SkillEntry[]; - builtAt?: number; -}): SkillIndex { - const indexed = params.entries.map((entry) => { - const trust = resolveSkillTrustInfo(entry); - return { - id: createSkillId({ - sourceKind: trust.sourceKind, - sourceLabel: trust.sourceLabel, - name: entry.skill.name, - path: entry.skill.filePath, - }), - name: entry.skill.name, - description: entry.skill.description, - path: entry.skill.filePath, - baseDir: entry.skill.baseDir, - sourceLabel: trust.sourceLabel, - sourceKind: trust.sourceKind, - owner: trust.owner, - writable: trust.writable, - writableReason: trust.writableReason, - entry, - } satisfies SkillIndexEntry; - }); - - const byId = new Map(); - const byName = new Map(); - const byPath = new Map(); - for (const item of indexed) { - byId.set(item.id, item); - byPath.set(item.path, item); - const named = byName.get(item.name); - if (named) { - named.push(item); - } else { - byName.set(item.name, [item]); - } - } - - return { - cacheKey: params.cacheKey, - builtAt: params.builtAt ?? Date.now(), - entries: indexed, - byId, - byName, - byPath, - }; -} - -export function skillIndexEntries(index: SkillIndex): SkillEntry[] { - return index.entries.map((entry) => entry.entry); -} - -export function skillIndexResolvedSkills(index: SkillIndex): Skill[] { - return index.entries.map((entry) => entry.entry.skill); -} diff --git a/src/skills/discovery/service.test.ts b/src/skills/discovery/service.test.ts deleted file mode 100644 index c78628dba57..00000000000 --- a/src/skills/discovery/service.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { buildWorkspaceSkillSnapshot as buildLegacyWorkspaceSkillSnapshot } from "../loading/workspace.js"; -import { writeWorkspaceSkills } from "../test-support/e2e-test-helpers.js"; -import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js"; -import type { SkillEntry } from "../types.js"; -import { buildSkillIndexCacheKey, buildWorkspaceSkillSnapshot, SkillsService } from "./service.js"; - -const tempDirs: string[] = []; - -async function makeTempWorkspace(): Promise { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-service-")); - tempDirs.push(dir); - return dir; -} - -function isolatedSkillRoots(workspaceDir: string) { - return { - managedSkillsDir: path.join(workspaceDir, ".managed"), - bundledSkillsDir: path.join(workspaceDir, ".bundled"), - pluginSkillsDir: path.join(workspaceDir, ".plugin-skills"), - }; -} - -afterEach(async () => { - await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); -}); - -describe("SkillsService", () => { - it("does not load an index when the effective skill filter is empty", () => { - type LoadIndexPatch = { - loadIndex: () => never; - }; - const service = new SkillsService(); - const patch = service as unknown as LoadIndexPatch; - patch.loadIndex = () => { - throw new Error("empty skill filters must not scan skill roots"); - }; - const config = { - agents: { defaults: { skills: [] } }, - } satisfies OpenClawConfig; - - const explicit = service.buildSnapshot("/workspace/that/does/not/exist", { - skillFilter: [], - snapshotVersion: 11, - }); - const fromConfig = service.buildSnapshot("/workspace/that/does/not/exist", { - config, - agentId: "demo-agent", - snapshotVersion: 12, - }); - - expect(explicit).toMatchObject({ prompt: "", skills: [], resolvedSkills: [], version: 11 }); - expect(fromConfig).toMatchObject({ prompt: "", skills: [], resolvedSkills: [], version: 12 }); - }); - - it("does not apply default skill filters without an agent id", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-default-filter", description: "Default filter workflow" }, - ]); - const roots = isolatedSkillRoots(workspaceDir); - const config = { - agents: { defaults: { skills: [] } }, - } satisfies OpenClawConfig; - - const actual = buildWorkspaceSkillSnapshot(workspaceDir, { - ...roots, - config, - snapshotVersion: 13, - }); - const expected = buildLegacyWorkspaceSkillSnapshot(workspaceDir, { - ...roots, - config, - snapshotVersion: 13, - }); - - expect(actual.prompt).toBe(expected.prompt); - expect(actual.skills).toEqual(expected.skills); - expect(actual.resolvedSkills).toEqual(expected.resolvedSkills); - expect(actual.skills.map((skill) => skill.name)).toContain("service-default-filter"); - }); - - it("builds snapshots from the index without changing prompt output", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "alpha", description: "Alpha workflow" }, - { name: "beta", description: "Beta workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - - const actual = service.buildSnapshot(workspaceDir, { - ...roots, - snapshotVersion: 42, - }); - const expected = buildLegacyWorkspaceSkillSnapshot(workspaceDir, { - ...roots, - snapshotVersion: 42, - }); - - expect(actual.prompt).toBe(expected.prompt); - expect(actual.skills).toEqual(expected.skills); - expect(actual.resolvedSkills).toEqual(expected.resolvedSkills); - expect(actual.version).toBe(42); - }); - - it("preserves the preloaded entries snapshot path", () => { - const entry: SkillEntry = { - skill: createCanonicalFixtureSkill({ - name: "synthetic-entry-skill", - description: "Synthetic entry", - filePath: "/synthetic/skills/synthetic-entry-skill/SKILL.md", - baseDir: "/synthetic/skills/synthetic-entry-skill", - source: "openclaw-workspace", - }), - frontmatter: {}, - }; - - const snapshot = buildWorkspaceSkillSnapshot("/workspace/that/does/not/exist", { - entries: [entry], - snapshotVersion: 9, - }); - - expect(snapshot.prompt).toContain("synthetic-entry-skill"); - expect(snapshot.skills).toEqual([{ name: "synthetic-entry-skill" }]); - expect(snapshot.resolvedSkills).toEqual([entry.skill]); - expect(snapshot.version).toBe(9); - }); - - it("caches the source-aware index until the version key changes", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-cache-alpha", description: "Alpha workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - - const first = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 1 }); - const second = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 1 }); - const third = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 2 }); - const firstAgain = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 1 }); - - expect(second).toBe(first); - expect(third).not.toBe(first); - expect(firstAgain).not.toBe(first); - expect(first.entries.find((entry) => entry.name === "service-cache-alpha")).toMatchObject({ - name: "service-cache-alpha", - sourceKind: "workspace", - owner: "workspace", - writable: true, - writableReason: "workspace-owned-skill", - }); - }); - - it("does not cache generation zero because it means no invalidation has happened yet", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-zero-alpha", description: "Alpha workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - - const before = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 0 }); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-zero-beta", description: "Beta workflow" }, - ]); - const after = service.getIndex({ workspaceDir, ...roots, snapshotVersion: 0 }); - - expect(after).not.toBe(before); - expect(before.entries.map((entry) => entry.name)).not.toContain("service-zero-beta"); - expect(after.entries.map((entry) => entry.name)).toContain("service-zero-beta"); - }); - - it("does not cache explicit indexes when skill watching is disabled", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-watch-alpha", description: "Alpha workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - const config = { skills: { load: { watch: false } } } satisfies OpenClawConfig; - - const before = service.getIndex({ workspaceDir, ...roots, config, snapshotVersion: 1 }); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-watch-beta", description: "Beta workflow" }, - ]); - const after = service.getIndex({ workspaceDir, ...roots, config, snapshotVersion: 1 }); - - expect(after).not.toBe(before); - expect(before.entries.map((entry) => entry.name)).not.toContain("service-watch-beta"); - expect(after.entries.map((entry) => entry.name)).toContain("service-watch-beta"); - }); - - it("keeps snapshots uncached even when callers pass a positive version", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-snapshot-alpha", description: "Alpha workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - - const before = service.buildSnapshot(workspaceDir, { ...roots, snapshotVersion: 1 }); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-snapshot-beta", description: "Beta workflow" }, - ]); - const after = service.buildSnapshot(workspaceDir, { ...roots, snapshotVersion: 1 }); - - expect(before.skills.map((skill) => skill.name)).not.toContain("service-snapshot-beta"); - expect(after.skills.map((skill) => skill.name)).toContain("service-snapshot-beta"); - }); - - it("bounds cached indexes across workspace scopes", async () => { - const service = new SkillsService(); - const workspaces = await Promise.all( - Array.from({ length: 17 }, async (_, index) => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: `service-lru-${index}`, description: "Cached workflow" }, - ]); - return workspaceDir; - }), - ); - const firstWorkspace = workspaces[0]; - const first = service.getIndex({ - workspaceDir: firstWorkspace, - ...isolatedSkillRoots(firstWorkspace), - snapshotVersion: 1, - }); - - for (const workspaceDir of workspaces.slice(1)) { - service.getIndex({ - workspaceDir, - ...isolatedSkillRoots(workspaceDir), - snapshotVersion: 1, - }); - } - const firstAgain = service.getIndex({ - workspaceDir: firstWorkspace, - ...isolatedSkillRoots(firstWorkspace), - snapshotVersion: 1, - }); - - expect(firstAgain).not.toBe(first); - }); - - it("includes plugin config in the versioned cache key", async () => { - const workspaceDir = await makeTempWorkspace(); - const roots = isolatedSkillRoots(workspaceDir); - const enabledConfig = { - plugins: { entries: { demo: { enabled: true } } }, - } satisfies OpenClawConfig; - const disabledConfig = { - plugins: { entries: { demo: { enabled: false } } }, - } satisfies OpenClawConfig; - - const enabledKey = buildSkillIndexCacheKey({ - workspaceDir, - ...roots, - config: enabledConfig, - snapshotVersion: 1, - }); - const disabledKey = buildSkillIndexCacheKey({ - workspaceDir, - ...roots, - config: disabledConfig, - snapshotVersion: 1, - }); - - expect(enabledKey).not.toBe(disabledKey); - }); - - it("keeps legacy snapshot calls uncached unless a version is supplied", async () => { - const workspaceDir = await makeTempWorkspace(); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-legacy-alpha", description: "Alpha workflow" }, - ]); - const service = new SkillsService(); - const roots = isolatedSkillRoots(workspaceDir); - - const before = service.buildSnapshot(workspaceDir, roots); - await writeWorkspaceSkills(workspaceDir, [ - { name: "service-legacy-beta", description: "Beta workflow" }, - ]); - const after = service.buildSnapshot(workspaceDir, roots); - const beforeNames = before.skills.map((skill) => skill.name); - const afterNames = after.skills.map((skill) => skill.name); - - expect(beforeNames).toContain("service-legacy-alpha"); - expect(beforeNames).not.toContain("service-legacy-beta"); - expect(afterNames).toContain("service-legacy-alpha"); - expect(afterNames).toContain("service-legacy-beta"); - }); -}); diff --git a/src/skills/discovery/service.ts b/src/skills/discovery/service.ts deleted file mode 100644 index 3b8a3c162a7..00000000000 --- a/src/skills/discovery/service.ts +++ /dev/null @@ -1,253 +0,0 @@ -import crypto from "node:crypto"; -import path from "node:path"; -import { stableStringify } from "../../agents/stable-stringify.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js"; -import { - buildWorkspaceSkillSnapshot as buildWorkspaceSkillSnapshotFromEntries, - loadWorkspaceSkillEntries, -} from "../loading/workspace.js"; -import { getSkillsSnapshotVersion } from "../runtime/refresh-state.js"; -import type { SkillEligibilityContext, SkillEntry, SkillSnapshot } from "../types.js"; -import { resolveEffectiveAgentSkillFilter } from "./agent-filter.js"; -import { normalizeSkillFilter } from "./filter.js"; -import { buildSkillIndex, skillIndexEntries, type SkillIndex } from "./registry.js"; - -const MAX_SKILL_INDEX_CACHE_ENTRIES = 16; - -export type SkillIndexRequest = { - workspaceDir: string; - config?: OpenClawConfig; - managedSkillsDir?: string; - bundledSkillsDir?: string; - pluginSkillsDir?: string; - snapshotVersion?: number; -}; - -export type SkillSnapshotBuildOptions = { - config?: OpenClawConfig; - managedSkillsDir?: string; - bundledSkillsDir?: string; - pluginSkillsDir?: string; - entries?: SkillEntry[]; - agentId?: string; - skillFilter?: string[]; - eligibility?: SkillEligibilityContext; - snapshotVersion?: number; -}; - -export class SkillsService { - private readonly cache = new Map(); - private readonly cacheScopes = new Map(); - - getIndex(request: SkillIndexRequest): SkillIndex { - const snapshotVersion = - request.snapshotVersion ?? getSkillsSnapshotVersion(request.workspaceDir); - if (!shouldCacheSkillIndex(request, snapshotVersion)) { - return this.loadIndex(request, buildUncachedSkillIndexCacheKey(request, snapshotVersion)); - } - const cacheKeyParts = buildSkillIndexCacheKeyParts(request, snapshotVersion); - const cacheKey = stringifyCacheKeyParts(cacheKeyParts); - const cached = this.cache.get(cacheKey); - if (cached) { - this.cache.delete(cacheKey); - this.cache.set(cacheKey, cached); - return cached; - } - const index = this.loadIndex(request, cacheKey); - this.pruneScope(cacheKeyParts.scope, cacheKey); - this.cache.set(cacheKey, index); - this.cacheScopes.set(cacheKey, cacheKeyParts.scope); - this.pruneCapacity(); - return index; - } - - private loadIndex(request: SkillIndexRequest, cacheKey: string): SkillIndex { - const entries = loadWorkspaceSkillEntries(request.workspaceDir, { - config: request.config, - managedSkillsDir: request.managedSkillsDir, - bundledSkillsDir: request.bundledSkillsDir, - pluginSkillsDir: request.pluginSkillsDir, - }); - return buildSkillIndex({ cacheKey, entries }); - } - - buildSnapshot(workspaceDir: string, opts?: SkillSnapshotBuildOptions): SkillSnapshot { - if (opts?.entries || hasEmptyEffectiveSkillFilter(opts)) { - return buildWorkspaceSkillSnapshotFromEntries(workspaceDir, opts); - } - const request = { - workspaceDir, - config: opts?.config, - managedSkillsDir: opts?.managedSkillsDir, - bundledSkillsDir: opts?.bundledSkillsDir, - pluginSkillsDir: opts?.pluginSkillsDir, - snapshotVersion: opts?.snapshotVersion, - }; - const snapshotVersion = request.snapshotVersion ?? getSkillsSnapshotVersion(workspaceDir); - const index = this.loadIndex( - request, - buildUncachedSkillIndexCacheKey(request, snapshotVersion), - ); - return buildSkillSnapshotFromIndex(workspaceDir, index, opts); - } - - invalidate(): void { - this.cache.clear(); - this.cacheScopes.clear(); - } - - private pruneScope(scope: string, keepKey: string): void { - for (const [key, cachedScope] of this.cacheScopes) { - if (key === keepKey || cachedScope !== scope) { - continue; - } - this.cache.delete(key); - this.cacheScopes.delete(key); - } - } - - private pruneCapacity(): void { - while (this.cache.size > MAX_SKILL_INDEX_CACHE_ENTRIES) { - const oldestKey = this.cache.keys().next().value; - if (oldestKey === undefined) { - break; - } - this.cache.delete(oldestKey); - this.cacheScopes.delete(oldestKey); - } - } -} - -export const skillsService = new SkillsService(); - -export function buildSkillSnapshotFromIndex( - workspaceDir: string, - index: SkillIndex, - opts?: SkillSnapshotBuildOptions, -): SkillSnapshot { - return buildWorkspaceSkillSnapshotFromEntries(workspaceDir, { - entries: skillIndexEntries(index), - config: opts?.config, - agentId: opts?.agentId, - skillFilter: opts?.skillFilter, - eligibility: opts?.eligibility, - snapshotVersion: opts?.snapshotVersion, - }); -} - -export function buildWorkspaceSkillSnapshot( - workspaceDir: string, - opts?: SkillSnapshotBuildOptions, -): SkillSnapshot { - return skillsService.buildSnapshot(workspaceDir, opts); -} - -function stableHash(value: unknown): string { - return crypto.createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 16); -} - -function normalizedOptionalPath(value?: string): string { - return value ? path.resolve(value) : ""; -} - -function hasEmptyEffectiveSkillFilter(opts?: SkillSnapshotBuildOptions): boolean { - if (opts?.skillFilter !== undefined) { - const filter = normalizeSkillFilter(opts.skillFilter); - return filter !== undefined && filter.length === 0; - } - if (!opts?.config || !opts.agentId) { - return false; - } - const filter = resolveEffectiveAgentSkillFilter(opts.config, opts.agentId); - return filter !== undefined && filter.length === 0; -} - -export function buildSkillIndexCacheKey(request: SkillIndexRequest): string { - const snapshotVersion = request.snapshotVersion ?? getSkillsSnapshotVersion(request.workspaceDir); - return stringifyCacheKeyParts(buildSkillIndexCacheKeyParts(request, snapshotVersion)); -} - -type SkillIndexCacheKeyParts = { - scope: string; - snapshotVersion: number; -}; - -function shouldCacheSkillIndex( - request: SkillIndexRequest, - snapshotVersion: number | undefined, -): boolean { - return ( - typeof snapshotVersion === "number" && - snapshotVersion > 0 && - request.config?.skills?.load?.watch !== false - ); -} - -function buildUncachedSkillIndexCacheKey( - request: SkillIndexRequest, - snapshotVersion: number, -): string { - return stableStringify({ - uncached: true, - workspaceDir: path.resolve(request.workspaceDir), - snapshotVersion, - }); -} - -function buildSkillIndexCacheKeyParts( - request: SkillIndexRequest, - snapshotVersion: number, -): SkillIndexCacheKeyParts { - const scope = stableStringify({ - workspaceDir: path.resolve(request.workspaceDir), - managedSkillsDir: normalizedOptionalPath(request.managedSkillsDir), - bundledSkillsDir: normalizedOptionalPath(request.bundledSkillsDir), - pluginSkillsDir: normalizedOptionalPath(request.pluginSkillsDir), - config: stableHash(request.config ?? {}), - pluginDiscovery: resolvePluginSkillDiscoveryFingerprint(request), - }); - return { scope, snapshotVersion }; -} - -function stringifyCacheKeyParts(parts: SkillIndexCacheKeyParts): string { - return stableStringify(parts); -} - -function resolvePluginSkillDiscoveryFingerprint(request: SkillIndexRequest): string { - const snapshot = resolvePluginMetadataSnapshot({ - workspaceDir: request.workspaceDir, - config: request.config ?? {}, - env: process.env, - allowWorkspaceScopedCurrent: true, - }); - return stableHash({ - policyHash: snapshot.policyHash, - configFingerprint: snapshot.configFingerprint ?? null, - registrySource: snapshot.registrySource ?? null, - index: { - hostContractVersion: snapshot.index.hostContractVersion, - compatRegistryVersion: snapshot.index.compatRegistryVersion, - migrationVersion: snapshot.index.migrationVersion, - policyHash: snapshot.index.policyHash, - installRecords: snapshot.index.installRecords, - plugins: snapshot.index.plugins.map((plugin) => ({ - pluginId: plugin.pluginId, - enabled: plugin.enabled, - enabledByDefault: plugin.enabledByDefault ?? null, - manifestHash: plugin.manifestHash, - manifestPath: plugin.manifestPath, - origin: plugin.origin, - rootDir: plugin.rootDir, - })), - }, - manifestPlugins: snapshot.manifestRegistry.plugins.map((plugin) => ({ - id: plugin.id, - enabledByDefault: plugin.enabledByDefault ?? null, - kind: plugin.kind ?? null, - origin: plugin.origin, - rootDir: plugin.rootDir, - skills: plugin.skills, - })), - }); -} diff --git a/src/skills/discovery/trust.ts b/src/skills/discovery/trust.ts deleted file mode 100644 index 4f76bbe5edb..00000000000 --- a/src/skills/discovery/trust.ts +++ /dev/null @@ -1,107 +0,0 @@ -import path from "node:path"; -import { resolveSkillSource } from "../loading/source.js"; -import type { SkillEntry } from "../types.js"; - -export type SkillSourceKind = - | "workspace" - | "generated" - | "bundled" - | "clawhub" - | "plugin" - | "extra" - | "system"; - -export type SkillWritablePolicy = { - writable: boolean; - reason: string; -}; - -export type SkillTrustInfo = { - sourceLabel: string; - sourceKind: SkillSourceKind; - owner: string; - writable: boolean; - writableReason: string; -}; - -export function classifySkillSourceKind(sourceLabel: string): SkillSourceKind { - switch (sourceLabel) { - case "openclaw-workspace": - case "agents-skills-project": - return "workspace"; - case "openclaw-managed": - return "clawhub"; - case "openclaw-bundled": - return "bundled"; - case "agents-skills-personal": - case "openclaw-extra": - return "extra"; - default: - return "extra"; - } -} - -export function resolveSkillOwner(params: { - sourceKind: SkillSourceKind; - sourceLabel: string; - skillPath: string; -}): string { - if (params.sourceKind === "workspace") { - return "workspace"; - } - if (params.sourceKind === "generated") { - return "workspace"; - } - if (params.sourceKind === "bundled") { - return "openclaw-release"; - } - if (params.sourceKind === "clawhub") { - return "clawhub"; - } - if (params.sourceKind === "plugin") { - return "plugin"; - } - if (params.sourceKind === "system") { - return "openclaw-system"; - } - if (params.sourceLabel === "agents-skills-personal") { - return "user"; - } - return path.basename(path.dirname(params.skillPath)) || "extra"; -} - -export function resolveSkillWritablePolicy(sourceKind: SkillSourceKind): SkillWritablePolicy { - switch (sourceKind) { - case "workspace": - case "generated": - return { writable: true, reason: "workspace-owned-skill" }; - case "bundled": - return { writable: false, reason: "release-owned-skill" }; - case "clawhub": - return { writable: false, reason: "installer-owned-skill" }; - case "plugin": - return { writable: false, reason: "plugin-owned-skill" }; - case "system": - return { writable: false, reason: "system-owned-skill" }; - case "extra": - return { writable: false, reason: "extra-root-load-only" }; - } - return { writable: false, reason: "unknown-source-load-only" }; -} - -export function resolveSkillTrustInfo(entry: SkillEntry): SkillTrustInfo { - const sourceLabel = resolveSkillSource(entry.skill); - const sourceKind = classifySkillSourceKind(sourceLabel); - const writable = resolveSkillWritablePolicy(sourceKind); - return { - sourceLabel, - sourceKind, - owner: resolveSkillOwner({ - sourceKind, - sourceLabel, - skillPath: entry.skill.filePath, - }), - writable: writable.writable, - writableReason: writable.reason, - }; -} diff --git a/src/skills/index.ts b/src/skills/index.ts deleted file mode 100644 index da3997a0acd..00000000000 --- a/src/skills/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -export { - hasBinary, - isBundledSkillAllowed, - isConfigPathTruthy, - resolveBundledAllowlist, - resolveConfigPath, - resolveRuntimePlatform, - resolveSkillConfig, - resolveSkillsInstallPreferences, -} from "./loading/config.js"; -export { - applySkillEnvOverrides, - applySkillEnvOverridesFromSnapshot, -} from "./runtime/env-overrides.js"; -export type { - OpenClawSkillMetadata, - SkillEligibilityContext, - SkillCommandSpec, - SkillEntry, - SkillInstallSpec, - SkillSnapshot, - SkillTelemetrySource, - SkillsInstallPreferences, -} from "./types.js"; -export { - buildWorkspaceSkillsPrompt, - filterWorkspaceSkillEntries, - filterWorkspaceSkillEntriesWithOptions, - loadWorkspaceSkillEntries, - resolveSkillsPromptForRun, - syncSkillsToWorkspace, -} from "./loading/workspace.js"; -export { buildWorkspaceSkillCommandSpecs } from "./discovery/command-specs.js"; -export type { - LoadSkillsFromDirOptions, - LoadSkillsOptions, - LoadSkillsResult, - Skill, - SkillFrontmatter, -} from "./loading/session.js"; -export { - formatSkillsForPrompt as formatSessionSkillsForPrompt, - loadSkills, -} from "./loading/session.js"; -export type { SkillIndex, SkillIndexEntry } from "./discovery/registry.js"; -export { - buildSkillIndex, - skillIndexEntries, - skillIndexResolvedSkills, -} from "./discovery/registry.js"; -export type { SkillSourceKind, SkillTrustInfo, SkillWritablePolicy } from "./discovery/trust.js"; -export { - classifySkillSourceKind, - resolveSkillOwner, - resolveSkillTrustInfo, - resolveSkillWritablePolicy, -} from "./discovery/trust.js"; -export type { SkillIndexRequest, SkillSnapshotBuildOptions } from "./discovery/service.js"; -export { - SkillsService, - buildSkillIndexCacheKey, - buildSkillSnapshotFromIndex, - buildWorkspaceSkillSnapshot, - skillsService, -} from "./discovery/service.js"; diff --git a/src/skills/runtime/session-snapshot.ts b/src/skills/runtime/session-snapshot.ts index 07125bfa08c..e06a153a092 100644 --- a/src/skills/runtime/session-snapshot.ts +++ b/src/skills/runtime/session-snapshot.ts @@ -3,7 +3,7 @@ import { stableStringify } from "../../agents/stable-stringify.js"; import { redactConfigObject } from "../../config/redact-snapshot.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { matchesSkillFilter } from "../discovery/filter.js"; -import { buildWorkspaceSkillSnapshot } from "../discovery/service.js"; +import { buildWorkspaceSkillSnapshot } from "../loading/workspace.js"; import type { SkillEligibilityContext, SkillSnapshot } from "../types.js"; import { getSkillsSnapshotVersion, shouldRefreshSnapshotForVersion } from "./refresh-state.js"; import { ensureSkillsWatcher } from "./refresh.js";