mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-26 17:32:16 +00:00
CLI/completion: fix generator OOM and harden plugin registries (#45537)
* fix: avoid OOM during completion script generation * CLI/completion: fix PowerShell nested command paths * CLI/completion: cover generated shell scripts * Changelog: note completion generator follow-up * Plugins: reserve shared registry names --------- Co-authored-by: Xiaoyi <xiaoyi@example.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -986,6 +986,153 @@ describe("loadOpenClawPlugins", () => {
|
||||
expect(httpPlugin?.httpRoutes).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects duplicate plugin-visible hook names", () => {
|
||||
useNoBundledPlugins();
|
||||
const first = writePlugin({
|
||||
id: "hook-owner-a",
|
||||
filename: "hook-owner-a.cjs",
|
||||
body: `module.exports = { id: "hook-owner-a", register(api) {
|
||||
api.registerHook("gateway:startup", () => {}, { name: "shared-hook" });
|
||||
} };`,
|
||||
});
|
||||
const second = writePlugin({
|
||||
id: "hook-owner-b",
|
||||
filename: "hook-owner-b.cjs",
|
||||
body: `module.exports = { id: "hook-owner-b", register(api) {
|
||||
api.registerHook("gateway:startup", () => {}, { name: "shared-hook" });
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [first.file, second.file] },
|
||||
allow: ["hook-owner-a", "hook-owner-b"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.hooks.filter((entry) => entry.entry.hook.name === "shared-hook")).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.level === "error" &&
|
||||
diag.pluginId === "hook-owner-b" &&
|
||||
diag.message === "hook already registered: shared-hook (hook-owner-a)",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects duplicate plugin service ids", () => {
|
||||
useNoBundledPlugins();
|
||||
const first = writePlugin({
|
||||
id: "service-owner-a",
|
||||
filename: "service-owner-a.cjs",
|
||||
body: `module.exports = { id: "service-owner-a", register(api) {
|
||||
api.registerService({ id: "shared-service", start() {} });
|
||||
} };`,
|
||||
});
|
||||
const second = writePlugin({
|
||||
id: "service-owner-b",
|
||||
filename: "service-owner-b.cjs",
|
||||
body: `module.exports = { id: "service-owner-b", register(api) {
|
||||
api.registerService({ id: "shared-service", start() {} });
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [first.file, second.file] },
|
||||
allow: ["service-owner-a", "service-owner-b"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.services.filter((entry) => entry.service.id === "shared-service")).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.level === "error" &&
|
||||
diag.pluginId === "service-owner-b" &&
|
||||
diag.message === "service already registered: shared-service (service-owner-a)",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires plugin CLI registrars to declare explicit command roots", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "cli-missing-metadata",
|
||||
filename: "cli-missing-metadata.cjs",
|
||||
body: `module.exports = { id: "cli-missing-metadata", register(api) {
|
||||
api.registerCli(() => {});
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["cli-missing-metadata"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars).toHaveLength(0);
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.level === "error" &&
|
||||
diag.pluginId === "cli-missing-metadata" &&
|
||||
diag.message === "cli registration missing explicit commands metadata",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects duplicate plugin CLI command roots", () => {
|
||||
useNoBundledPlugins();
|
||||
const first = writePlugin({
|
||||
id: "cli-owner-a",
|
||||
filename: "cli-owner-a.cjs",
|
||||
body: `module.exports = { id: "cli-owner-a", register(api) {
|
||||
api.registerCli(() => {}, { commands: ["shared-cli"] });
|
||||
} };`,
|
||||
});
|
||||
const second = writePlugin({
|
||||
id: "cli-owner-b",
|
||||
filename: "cli-owner-b.cjs",
|
||||
body: `module.exports = { id: "cli-owner-b", register(api) {
|
||||
api.registerCli(() => {}, { commands: ["shared-cli"] });
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [first.file, second.file] },
|
||||
allow: ["cli-owner-a", "cli-owner-b"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars).toHaveLength(1);
|
||||
expect(registry.cliRegistrars[0]?.pluginId).toBe("cli-owner-a");
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.level === "error" &&
|
||||
diag.pluginId === "cli-owner-b" &&
|
||||
diag.message === "cli command already registered: shared-cli (cli-owner-a)",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("registers http routes", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
|
||||
@@ -238,6 +238,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existingHook = registry.hooks.find((entry) => entry.entry.hook.name === name);
|
||||
if (existingHook) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `hook already registered: ${name} (${existingHook.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const description = entry?.hook.description ?? opts?.description ?? "";
|
||||
const hookEntry: HookEntry = entry
|
||||
@@ -473,6 +483,28 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
opts?: { commands?: string[] },
|
||||
) => {
|
||||
const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean);
|
||||
if (commands.length === 0) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "cli registration missing explicit commands metadata",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existing = registry.cliRegistrars.find((entry) =>
|
||||
entry.commands.some((command) => commands.includes(command)),
|
||||
);
|
||||
if (existing) {
|
||||
const overlap = commands.find((command) => existing.commands.includes(command));
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `cli command already registered: ${overlap ?? commands[0]} (${existing.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
record.cliCommands.push(...commands);
|
||||
registry.cliRegistrars.push({
|
||||
pluginId: record.id,
|
||||
@@ -487,6 +519,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const existing = registry.services.find((entry) => entry.service.id === id);
|
||||
if (existing) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `service already registered: ${id} (${existing.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
record.services.push(id);
|
||||
registry.services.push({
|
||||
pluginId: record.id,
|
||||
|
||||
Reference in New Issue
Block a user