mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
fix: enforce strict config validation
This commit is contained in:
@@ -10,6 +10,7 @@ type TempPlugin = { dir: string; file: string; id: string };
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const prevBundledDir = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR;
|
||||
const EMPTY_CONFIG_SCHEMA = `configSchema: { safeParse() { return { success: true, data: {} }; }, jsonSchema: { type: "object", additionalProperties: false, properties: {} } },`;
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = path.join(os.tmpdir(), `clawdbot-plugin-${randomUUID()}`);
|
||||
@@ -44,7 +45,11 @@ describe("loadClawdbotPlugins", () => {
|
||||
it("disables bundled plugins by default", () => {
|
||||
const bundledDir = makeTempDir();
|
||||
const bundledPath = path.join(bundledDir, "bundled.ts");
|
||||
fs.writeFileSync(bundledPath, "export default function () {}", "utf-8");
|
||||
fs.writeFileSync(
|
||||
bundledPath,
|
||||
`export default { id: "bundled", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -100,7 +105,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
const bundledPath = path.join(bundledDir, "memory-core.ts");
|
||||
fs.writeFileSync(
|
||||
bundledPath,
|
||||
'export default { id: "memory-core", kind: "memory", register() {} };',
|
||||
`export default { id: "memory-core", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||
@@ -137,7 +142,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.ts"),
|
||||
'export default { id: "memory-core", kind: "memory", name: "Memory (Core)", register() {} };',
|
||||
`export default { id: "memory-core", kind: "memory", name: "Memory (Core)", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
@@ -164,7 +169,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
id: "allowed",
|
||||
body: `export default function (api) { api.registerGatewayMethod("allowed.ping", ({ respond }) => respond(true, { ok: true })); }`,
|
||||
body: `export default { id: "allowed", ${EMPTY_CONFIG_SCHEMA} register(api) { api.registerGatewayMethod("allowed.ping", ({ respond }) => respond(true, { ok: true })); } };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -187,7 +192,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
id: "blocked",
|
||||
body: `export default function () {}`,
|
||||
body: `export default { id: "blocked", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -237,7 +242,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
id: "channel-demo",
|
||||
body: `export default function (api) {
|
||||
body: `export default { id: "channel-demo", ${EMPTY_CONFIG_SCHEMA} register(api) {
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "demo",
|
||||
@@ -256,7 +261,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
outbound: { deliveryMode: "direct" }
|
||||
}
|
||||
});
|
||||
};`,
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -278,9 +283,9 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
id: "http-demo",
|
||||
body: `export default function (api) {
|
||||
body: `export default { id: "http-demo", ${EMPTY_CONFIG_SCHEMA} register(api) {
|
||||
api.registerHttpHandler(async () => false);
|
||||
};`,
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -304,7 +309,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const plugin = writePlugin({
|
||||
id: "config-disable",
|
||||
body: `export default function () {}`,
|
||||
body: `export default { id: "config-disable", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -327,11 +332,11 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const memoryA = writePlugin({
|
||||
id: "memory-a",
|
||||
body: `export default { id: "memory-a", kind: "memory", register() {} };`,
|
||||
body: `export default { id: "memory-a", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
const memoryB = writePlugin({
|
||||
id: "memory-b",
|
||||
body: `export default { id: "memory-b", kind: "memory", register() {} };`,
|
||||
body: `export default { id: "memory-b", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -354,7 +359,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const memory = writePlugin({
|
||||
id: "memory-off",
|
||||
body: `export default { id: "memory-off", kind: "memory", register() {} };`,
|
||||
body: `export default { id: "memory-off", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
@@ -373,12 +378,16 @@ describe("loadClawdbotPlugins", () => {
|
||||
|
||||
it("prefers higher-precedence plugins with the same id", () => {
|
||||
const bundledDir = makeTempDir();
|
||||
fs.writeFileSync(path.join(bundledDir, "shadow.js"), "export default function () {}", "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(bundledDir, "shadow.js"),
|
||||
`export default { id: "shadow", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||
|
||||
const override = writePlugin({
|
||||
id: "shadow",
|
||||
body: `export default function () {}`,
|
||||
body: `export default { id: "shadow", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||
});
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
|
||||
@@ -30,6 +30,7 @@ export type PluginLoadOptions = {
|
||||
logger?: PluginLogger;
|
||||
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
|
||||
cache?: boolean;
|
||||
mode?: "full" | "validate";
|
||||
};
|
||||
|
||||
type NormalizedPluginsConfig = {
|
||||
@@ -297,6 +298,7 @@ function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnost
|
||||
export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||
const cfg = options.config ?? {};
|
||||
const logger = options.logger ?? defaultLogger();
|
||||
const validateOnly = options.mode === "validate";
|
||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||
const cacheKey = buildCacheKey({
|
||||
workspaceDir: options.workspaceDir,
|
||||
@@ -437,6 +439,21 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
>)
|
||||
: undefined;
|
||||
|
||||
if (!definition?.configSchema) {
|
||||
logger.error(`[plugins] ${record.id} missing config schema`);
|
||||
record.status = "error";
|
||||
record.error = "missing config schema";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(candidate.idHint, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.kind === "memory" && memorySlot === record.id) {
|
||||
memorySlotMatched = true;
|
||||
}
|
||||
@@ -481,6 +498,12 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validateOnly) {
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(candidate.idHint, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof register !== "function") {
|
||||
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||
record.status = "error";
|
||||
|
||||
@@ -36,8 +36,10 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("resolvePluginTools optional tools", () => {
|
||||
const emptyConfigSchema =
|
||||
'configSchema: { safeParse() { return { success: true, data: {} }; }, jsonSchema: { type: "object", additionalProperties: false, properties: {} } },';
|
||||
const pluginBody = `
|
||||
export default function (api) {
|
||||
export default { ${emptyConfigSchema} register(api) {
|
||||
api.registerTool(
|
||||
{
|
||||
name: "optional_tool",
|
||||
@@ -49,7 +51,7 @@ export default function (api) {
|
||||
},
|
||||
{ optional: true },
|
||||
);
|
||||
}
|
||||
} }
|
||||
`;
|
||||
|
||||
it("skips optional tools without explicit allowlist", () => {
|
||||
@@ -138,7 +140,7 @@ export default function (api) {
|
||||
const plugin = writePlugin({
|
||||
id: "multi",
|
||||
body: `
|
||||
export default function (api) {
|
||||
export default { ${emptyConfigSchema} register(api) {
|
||||
api.registerTool({
|
||||
name: "message",
|
||||
description: "conflict",
|
||||
@@ -155,7 +157,7 @@ export default function (api) {
|
||||
return { content: [{ type: "text", text: "ok" }] };
|
||||
},
|
||||
});
|
||||
}
|
||||
} }
|
||||
`,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user