test: trim skills and bundle mcp overhead

This commit is contained in:
Peter Steinberger
2026-04-18 17:41:26 +01:00
parent 53239102f8
commit fa2f53993a
9 changed files with 37 additions and 133 deletions

View File

@@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
createBundleMcpTempHarness,
@@ -12,7 +12,7 @@ import { prepareCliBundleMcpConfig } from "./bundle-mcp.js";
const tempHarness = createBundleMcpTempHarness();
afterEach(async () => {
afterAll(async () => {
await tempHarness.cleanup();
});

View File

@@ -42,16 +42,6 @@ const STRIP_COMPONENTS_ZIP_BUFFER = Buffer.from(
"UEsDBAoAAAAAAMOJVlwAAAAAAAAAAAAAAAAIAAAAcGFja2FnZS9QSwMECgAAAAAAw4lWXKwqk9gCAAAAAgAAABEAAABwYWNrYWdlL2hlbGxvLnR4dGhpUEsBAhQACgAAAAAAw4lWXAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAQAAAAAAAAAHBhY2thZ2UvUEsBAhQACgAAAAAAw4lWXKwqk9gCAAAAAgAAABEAAAAAAAAAAAAAAAAAJgAAAHBhY2thZ2UvaGVsbG8udHh0UEsFBgAAAAACAAIAdQAAAFcAAAAAAA==",
"base64",
);
const ZIP_SLIP_BUFFER = Buffer.from(
"UEsDBAoAAAAAAMOJVlwAAAAAAAAAAAAAAAADAAAALi4vUEsDBAoAAAAAAMOJVlwAAAAAAAAAAAAAAAARAAAALi4vb3V0c2lkZS13cml0ZS9QSwMECgAAAAAAw4lWXD3iZKoEAAAABAAAABoAAAAuLi9vdXRzaWRlLXdyaXRlL3B3bmVkLnR4dHB3bmRQSwECFAAKAAAAAADDiVZcAAAAAAAAAAAAAAAAAwAAAAAAAAAAABAAAAAAAAAALi4vUEsBAhQACgAAAAAAw4lWXAAAAAAAAAAAAAAAABEAAAAAAAAAAAAQAAAAIQAAAC4uL291dHNpZGUtd3JpdGUvUEsBAhQACgAAAAAAw4lWXD3iZKoEAAAABAAAABoAAAAAAAAAAAAAAAAAUAAAAC4uL291dHNpZGUtd3JpdGUvcHduZWQudHh0UEsFBgAAAAADAAMAuAAAAIwAAAAAAA==",
"base64",
);
const TAR_GZ_TRAVERSAL_BUFFER = Buffer.from(
// Prebuilt archive containing ../outside-write/pwned.txt.
"H4sIAK4xm2kAA+2VvU7DMBDH3UoIUWaYLXbcS5PYZegQEKhBRUBbIT4GZBpXCqJNSFySlSdgZed1eCgcUvFRaMsQgVD9k05nW3eWz8nfR0g1GMnY98RmEvlSVMllmAyFR2QqUUEAALUsnHlG7VcPtXwO+djEhm1YlJpAbYrBYAYDhKGoA8xiFEseqaPEUvihkGJanArr92fsk5eC3/x/YWl9GZUROuA9fNjBp3hMtoZWlNWU3SrL5k8/29LpdtvjYZbxqGx1IqT0vr7WCwaEh+GNIGEU3IkhH/YEKpXRxv3FQznsPxdQpGYaZFL/RzxtCu6JqFrYOzBX/wZ81n8NmEERTosocB4Lrn8T8ED6A9EwmHp0Wd1idQK2ZVIAm1ZshlvuttPeabonuyTlUkbkO7k2nGPXcYO9q+tkPzmPk4q1hTsqqXU2K+mDxit/fQ+Lyhf9F9795+tf/WoT/Z8yi+n+/xuoz+1p8Wk0Gs3i8QJSs3VlABAAAA==", // pragma: allowlist secret
"base64",
);
function buildEntry(name: string): SkillEntry {
const skillDir = path.join(workspaceDir, "skills", name);
const filePath = path.join(skillDir, "SKILL.md");
@@ -177,38 +167,6 @@ beforeEach(() => {
});
describe("installDownloadSpec extraction safety", () => {
it("rejects archive traversal writes outside targetDir", async () => {
for (const testCase of [
{
label: "zip-slip",
name: "zip-slip",
url: "https://example.invalid/evil.zip",
archive: "zip" as const,
buffer: ZIP_SLIP_BUFFER,
},
{
label: "tar-slip",
name: "tar-slip",
url: "https://example.invalid/evil",
archive: "tar.gz" as const,
buffer: TAR_GZ_TRAVERSAL_BUFFER,
},
]) {
const entry = buildEntry(testCase.name);
const targetDir = path.join(resolveSkillToolsRootDir(entry), "target");
const outsideWritePath = path.join(workspaceDir, "outside-write", "pwned.txt");
mockArchiveResponse(new Uint8Array(testCase.buffer));
const result = await installDownloadSkill({
...testCase,
targetDir,
});
expect(result.ok, testCase.label).toBe(false);
expect(await fileExists(outsideWritePath), testCase.label).toBe(false);
}
});
it("extracts zip with stripComponents safely", async () => {
const entry = buildEntry("zip-good");
const targetDir = path.join(resolveSkillToolsRootDir(entry), "target");
@@ -318,28 +276,6 @@ describe("installDownloadSpec extraction safety (tar.bz2)", () => {
expectedExtract: false,
expectedStderrSubstring: "link",
},
{
label: "rejects archives containing FIFO entries",
name: "tbz2-fifo",
url: "https://example.invalid/evil.tbz2",
listOutput: "evil-fifo\n",
verboseListOutput: "prw-r--r-- 0 0 0 0 Jan 1 00:00 evil-fifo\n",
extract: "reject" as const,
expectedOk: false,
expectedExtract: false,
expectedStderrSubstring: "link",
},
{
label: "rejects oversized extracted entries",
name: "tbz2-oversized",
url: "https://example.invalid/oversized.tbz2",
listOutput: "big.bin\n",
verboseListOutput: "-rw-r--r-- 0 0 0 314572800 Jan 1 00:00 big.bin\n",
extract: "reject" as const,
expectedOk: false,
expectedExtract: false,
expectedStderrSubstring: "archive entry extracted size exceeds limit",
},
{
label: "extracts safe archives with stripComponents",
name: "tbz2-ok",
@@ -351,17 +287,6 @@ describe("installDownloadSpec extraction safety (tar.bz2)", () => {
expectedOk: true,
expectedExtract: true,
},
{
label: "rejects stripComponents escapes",
name: "tbz2-strip-escape",
url: "https://example.invalid/evil.tbz2",
listOutput: "a/../b.txt\n",
verboseListOutput: "-rw-r--r-- 0 0 0 0 Jan 1 00:00 a/../b.txt\n",
stripComponents: 1,
extract: "reject" as const,
expectedOk: false,
expectedExtract: false,
},
]) {
const entry = buildEntry(testCase.name);
const targetDir = path.join(resolveSkillToolsRootDir(entry), "target");

View File

@@ -23,6 +23,10 @@ vi.mock("../security/skill-scanner.js", () => ({
scanDirectoryWithSummary: (...args: unknown[]) => scanDirectoryWithSummaryMock(...args),
}));
vi.mock("./skills/plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
async function writeInstallableSkill(workspaceDir: string, name: string): Promise<string> {
const skillDir = path.join(workspaceDir, "skills", name);
await fs.mkdir(skillDir, { recursive: true });

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { buildWorkspaceSkillsPrompt } from "./skills.js";
import { writeSkill } from "./skills.test-helpers.js";
import {
@@ -10,6 +10,10 @@ import {
type SkillsHomeEnvSnapshot,
} from "./skills/home-env.test-support.js";
vi.mock("./skills/plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
const tempDirs: string[] = [];
async function createTempDir(prefix: string) {

View File

@@ -1,11 +1,15 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { withEnv } from "../test-utils/env.js";
import { writeSkill } from "./skills.e2e-test-helpers.js";
import { buildWorkspaceSkillsPrompt, syncSkillsToWorkspace } from "./skills.js";
vi.mock("./skills/plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
async function pathExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);

View File

@@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { withPathResolutionEnv } from "../test-utils/env.js";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
@@ -12,6 +12,10 @@ import {
type SkillsHomeEnvSnapshot,
} from "./skills/home-env.test-support.js";
vi.mock("./skills/plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
const fixtureSuite = createFixtureSuite("openclaw-skills-snapshot-suite-");
let truncationWorkspaceTemplateDir = "";
let nestedRepoTemplateDir = "";

View File

@@ -38,12 +38,12 @@ afterEach(async () => {
setLoggerOverride(null);
loggingState.rawConsole = null;
resetLogger();
await Promise.all(
tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
});
afterAll(async () => {
await Promise.all(
tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
await restoreMockSkillsHomeEnv(envSnapshot, async () => {
if (fakeHome) {
await fs.rm(fakeHome, { recursive: true, force: true });
@@ -67,22 +67,6 @@ async function setupWorkspaceWithProsePlugin() {
return { workspaceDir, managedDir, bundledDir };
}
async function setupWorkspaceWithDiffsPlugin() {
const workspaceDir = await createTempWorkspaceDir();
const managedDir = path.join(workspaceDir, ".managed");
const bundledDir = path.join(workspaceDir, ".bundled");
const pluginRoot = path.join(workspaceDir, ".openclaw", "extensions", "diffs");
await writePluginWithSkill({
pluginRoot,
pluginId: "diffs",
skillId: "diffs",
skillDescription: "test",
});
return { workspaceDir, managedDir, bundledDir };
}
describe("loadWorkspaceSkillEntries", () => {
it("handles an empty managed skills dir without throwing", async () => {
const workspaceDir = await createTempWorkspaceDir();
@@ -131,38 +115,6 @@ describe("loadWorkspaceSkillEntries", () => {
expect(entries.map((entry) => entry.skill.name)).not.toContain("prose");
});
it("includes diffs plugin skill when the plugin is enabled", async () => {
const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin();
const entries = loadWorkspaceSkillEntries(workspaceDir, {
config: {
plugins: {
entries: { diffs: { enabled: true } },
},
},
managedSkillsDir: managedDir,
bundledSkillsDir: bundledDir,
});
expect(entries.map((entry) => entry.skill.name)).toContain("diffs");
});
it("excludes diffs plugin skill when the plugin is disabled", async () => {
const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin();
const entries = loadWorkspaceSkillEntries(workspaceDir, {
config: {
plugins: {
entries: { diffs: { enabled: false } },
},
},
managedSkillsDir: managedDir,
bundledSkillsDir: bundledDir,
});
expect(entries.map((entry) => entry.skill.name)).not.toContain("diffs");
});
it("falls back to the skill directory name when frontmatter omits name", async () => {
const workspaceDir = await createTempWorkspaceDir();
const skillDir = path.join(workspaceDir, "skills", "fallback-name");

View File

@@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
clearRuntimeConfigSnapshot,
setRuntimeConfigSnapshot,
@@ -27,6 +27,10 @@ import {
type SkillsHomeEnvSnapshot,
} from "./skills/home-env.test-support.js";
vi.mock("./skills/plugin-skills.js", () => ({
resolvePluginSkillDirs: () => [],
}));
const fixtureSuite = createFixtureSuite("openclaw-skills-suite-");
let tempHome: TempHomeEnv | null = null;
let skillsHomeEnv: SkillsHomeEnvSnapshot | null = null;

View File

@@ -13,7 +13,11 @@ import {
mergeBundlePathLists,
normalizeBundlePathList,
} from "./bundle-manifest.js";
import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js";
import {
hasExplicitPluginConfig,
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "./config-state.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
export type ClaudeBundleCommandSpec = {
@@ -169,6 +173,9 @@ export function loadEnabledClaudeBundleCommands(params: {
workspaceDir: string;
cfg?: OpenClawConfig;
}): ClaudeBundleCommandSpec[] {
if (!hasExplicitPluginConfig(params.cfg?.plugins)) {
return [];
}
const registry = loadPluginManifestRegistry({
workspaceDir: params.workspaceDir,
config: params.cfg,