mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(plugins): enforce manifest kinds for exclusive slots (#114537)
Normalize plugin manifests to the documented memory and context-engine kinds. Filter unsupported entries, preserve valid dual-kind declaration order, and collapse duplicates before runtime slot selection. Closes #114321 Co-authored-by: kevin2966n <147227108+kevin2966n@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5e4b0d3bea
commit
3799e70e97
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import JSON5 from "json5";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveMemorySlotDecision } from "./config-state.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
@@ -122,6 +123,100 @@ describe("loadPluginManifest JSON5 tolerance", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "a supported memory kind",
|
||||
rawKind: "memory",
|
||||
expectedKind: "memory",
|
||||
},
|
||||
{
|
||||
name: "a supported context-engine kind",
|
||||
rawKind: "context-engine",
|
||||
expectedKind: "context-engine",
|
||||
},
|
||||
{
|
||||
name: "both supported kinds in declaration order",
|
||||
rawKind: ["context-engine", "memory"],
|
||||
expectedKind: ["context-engine", "memory"],
|
||||
},
|
||||
{
|
||||
name: "duplicate memory kinds collapsed into one kind",
|
||||
rawKind: ["memory", "memory"],
|
||||
expectedKind: "memory",
|
||||
},
|
||||
{
|
||||
name: "duplicate context-engine kinds collapsed into one kind",
|
||||
rawKind: ["context-engine", "context-engine"],
|
||||
expectedKind: "context-engine",
|
||||
},
|
||||
{
|
||||
name: "supported kinds filtered from invalid and duplicate array entries",
|
||||
rawKind: ["memory", "unknown-kind", 42, "memory", "context-engine", null],
|
||||
expectedKind: ["memory", "context-engine"],
|
||||
},
|
||||
{
|
||||
name: "a valid memory kind retained alongside invalid entries",
|
||||
rawKind: ["unknown-kind", 42, "memory", null],
|
||||
expectedKind: "memory",
|
||||
},
|
||||
{
|
||||
name: "an unsupported scalar kind",
|
||||
rawKind: "unknown-kind",
|
||||
expectedKind: undefined,
|
||||
},
|
||||
{
|
||||
name: "an array containing only unsupported kinds",
|
||||
rawKind: ["unknown-kind", 42, null],
|
||||
expectedKind: undefined,
|
||||
},
|
||||
])("normalizes $name", ({ rawKind, expectedKind }) => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "kind-normalization",
|
||||
kind: rawKind,
|
||||
configSchema: { type: "object" },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = loadPluginManifest(dir, false);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.kind).toEqual(expectedKind);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps duplicate memory declarations subject to the exclusive memory slot", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "duplicate-memory",
|
||||
kind: ["memory", "memory"],
|
||||
configSchema: { type: "object" },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = loadPluginManifest(dir, false);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.kind).toBe("memory");
|
||||
expect(
|
||||
resolveMemorySlotDecision({
|
||||
id: result.manifest.id,
|
||||
kind: result.manifest.kind,
|
||||
slot: "memory-core",
|
||||
selectedId: "memory-core",
|
||||
}),
|
||||
).toEqual({ enabled: false, reason: 'memory slot set to "memory-core"' });
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes modelSupport metadata from the manifest", () => {
|
||||
const dir = makeTempDir();
|
||||
const json5Content = `{
|
||||
|
||||
+12
-6
@@ -29,6 +29,7 @@ const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const;
|
||||
const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024;
|
||||
const MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES = 512;
|
||||
const CORE_RESERVED_PLUGIN_IDS = new Set(["node-mcp"]);
|
||||
const VALID_PLUGIN_KINDS: ReadonlySet<string> = new Set<PluginKind>(["memory", "context-engine"]);
|
||||
|
||||
export function isCoreReservedPluginId(id: string): boolean {
|
||||
return CORE_RESERVED_PLUGIN_IDS.has(normalizePluginPolicyId(id));
|
||||
@@ -109,13 +110,18 @@ function setCachedPluginManifestLoadResult(
|
||||
}
|
||||
|
||||
function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined {
|
||||
if (typeof raw === "string") {
|
||||
return raw as PluginKind;
|
||||
const values = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : [];
|
||||
const kinds: PluginKind[] = [];
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string" || !VALID_PLUGIN_KINDS.has(value)) {
|
||||
continue;
|
||||
}
|
||||
const kind = value as PluginKind;
|
||||
if (!kinds.includes(kind)) {
|
||||
kinds.push(kind);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(raw) && raw.length > 0 && raw.every((k) => typeof k === "string")) {
|
||||
return raw.length === 1 ? (raw[0] as PluginKind) : (raw as PluginKind[]);
|
||||
}
|
||||
return undefined;
|
||||
return kinds.length === 0 ? undefined : kinds.length === 1 ? kinds[0] : kinds;
|
||||
}
|
||||
|
||||
export function loadPluginManifest(
|
||||
|
||||
Reference in New Issue
Block a user