diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 86678c8f46d4..9e02352075e9 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -2789,6 +2789,61 @@ module.exports = { id: "throws-after-import", register() {} };`, clearInternalHooks(); }); + it("isolates unreadable definition reload metadata without dropping healthy plugins", () => { + useNoBundledPlugins(); + const badPlugin = writePlugin({ + id: "bad-definition-reload", + filename: "bad-definition-reload.cjs", + body: `module.exports = { + id: "bad-definition-reload", + reload: Object.defineProperty({}, "restartPrefixes", { + get() { + throw new Error("definition reload restart prefixes exploded"); + }, + }), + register() { + globalThis.badDefinitionReloadRegistered = true; + }, + };`, + }); + const healthyPlugin = writePlugin({ + id: "healthy-after-bad-reload", + filename: "healthy-after-bad-reload.cjs", + body: `module.exports = { + id: "healthy-after-bad-reload", + register() { + globalThis.healthyAfterBadReloadRegistered = true; + }, + };`, + }); + const globals = globalThis as Record; + delete globals.badDefinitionReloadRegistered; + delete globals.healthyAfterBadReloadRegistered; + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + load: { paths: [badPlugin.file, healthyPlugin.file] }, + allow: [badPlugin.id, healthyPlugin.id], + }, + }, + onlyPluginIds: [badPlugin.id, healthyPlugin.id], + }); + + expect(registry.plugins.find((entry) => entry.id === badPlugin.id)?.status).toBe("error"); + expect(registry.plugins.find((entry) => entry.id === healthyPlugin.id)?.status).toBe("loaded"); + expect(globals.badDefinitionReloadRegistered).toBeUndefined(); + expect(globals.healthyAfterBadReloadRegistered).toBe(true); + expect(registry.reloads).toStrictEqual([]); + expectDiagnosticContaining({ + registry, + level: "error", + pluginId: badPlugin.id, + message: "definition reload restart prefixes exploded", + }); + }); + it("rolls back global side effects when registration fails", async () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 6d3a0ff529d7..7ed5f7d7e62c 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -2642,14 +2642,32 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi } if (registrationPlan.runFullActivationOnlyRegistrations) { - if (definition?.reload) { - registerReload(record, definition.reload); - } - for (const nodeHostCommand of definition?.nodeHostCommands ?? []) { - registerNodeHostCommand(record, nodeHostCommand); - } - for (const collector of definition?.securityAuditCollectors ?? []) { - registerSecurityAuditCollector(record, collector); + const registrySnapshot = snapshotPluginRegistry(registry); + try { + if (definition?.reload) { + registerReload(record, definition.reload); + } + for (const nodeHostCommand of definition?.nodeHostCommands ?? []) { + registerNodeHostCommand(record, nodeHostCommand); + } + for (const collector of definition?.securityAuditCollectors ?? []) { + registerSecurityAuditCollector(record, collector); + } + } catch (err) { + restorePluginRegistry(registry, registrySnapshot); + recordPluginError({ + logger, + registry, + record, + seenIds, + pluginId, + origin: candidate.origin, + phase: "load", + error: err, + logPrefix: `[plugins] ${record.id} failed to register plugin definition metadata from ${record.source}: `, + diagnosticMessagePrefix: "failed to register plugin definition metadata: ", + }); + continue; } }