diff --git a/src/extension-host/gateway-methods.test.ts b/src/extension-host/gateway-methods.test.ts new file mode 100644 index 00000000000..281e4525d95 --- /dev/null +++ b/src/extension-host/gateway-methods.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it, vi } from "vitest"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { + createExtensionHostGatewayExtraHandlers, + logExtensionHostPluginDiagnostics, + resolveExtensionHostGatewayMethods, +} from "./gateway-methods.js"; + +describe("resolveExtensionHostGatewayMethods", () => { + it("adds plugin methods without duplicating base methods", () => { + const registry = createEmptyPluginRegistry(); + registry.gatewayHandlers.health = vi.fn(); + registry.gatewayHandlers["plugin.echo"] = vi.fn(); + + expect( + resolveExtensionHostGatewayMethods({ + registry, + baseMethods: ["health", "config.get"], + }), + ).toEqual(["health", "config.get", "plugin.echo"]); + }); +}); + +describe("createExtensionHostGatewayExtraHandlers", () => { + it("lets caller-provided handlers override plugin handlers", () => { + const pluginHandler = vi.fn(); + const callerHandler = vi.fn(); + const registry = createEmptyPluginRegistry(); + registry.gatewayHandlers.demo = pluginHandler; + + const handlers = createExtensionHostGatewayExtraHandlers({ + registry, + extraHandlers: { demo: callerHandler, health: vi.fn() }, + }); + + expect(handlers.demo).toBe(callerHandler); + expect(handlers.health).toBeTypeOf("function"); + }); +}); + +describe("logExtensionHostPluginDiagnostics", () => { + it("routes error diagnostics to error and others to info", () => { + const log = { + info: vi.fn(), + error: vi.fn(), + }; + + logExtensionHostPluginDiagnostics({ + diagnostics: [ + { + level: "warn", + pluginId: "demo", + source: "bundled", + message: "warned", + }, + { + level: "error", + pluginId: "demo", + source: "bundled", + message: "failed", + }, + ], + log, + }); + + expect(log.info).toHaveBeenCalledWith("[plugins] warned (plugin=demo, source=bundled)"); + expect(log.error).toHaveBeenCalledWith("[plugins] failed (plugin=demo, source=bundled)"); + }); +}); diff --git a/src/extension-host/gateway-methods.ts b/src/extension-host/gateway-methods.ts new file mode 100644 index 00000000000..921e43f9f3d --- /dev/null +++ b/src/extension-host/gateway-methods.ts @@ -0,0 +1,46 @@ +import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js"; +import type { PluginRegistry } from "../plugins/registry.js"; +import type { PluginDiagnostic } from "../plugins/types.js"; + +export function resolveExtensionHostGatewayMethods(params: { + registry: PluginRegistry; + baseMethods: string[]; +}): string[] { + const pluginMethods = Object.keys(params.registry.gatewayHandlers); + return Array.from(new Set([...params.baseMethods, ...pluginMethods])); +} + +export function createExtensionHostGatewayExtraHandlers(params: { + registry: PluginRegistry; + extraHandlers?: GatewayRequestHandlers; +}): GatewayRequestHandlers { + return { + ...params.registry.gatewayHandlers, + ...params.extraHandlers, + }; +} + +export function logExtensionHostPluginDiagnostics(params: { + diagnostics: PluginDiagnostic[]; + log: { + info: (msg: string) => void; + error: (msg: string) => void; + }; +}): void { + for (const diag of params.diagnostics) { + const details = [ + diag.pluginId ? `plugin=${diag.pluginId}` : null, + diag.source ? `source=${diag.source}` : null, + ] + .filter((entry): entry is string => Boolean(entry)) + .join(", "); + const message = details + ? `[plugins] ${diag.message} (${details})` + : `[plugins] ${diag.message}`; + if (diag.level === "error") { + params.log.error(message); + continue; + } + params.log.info(message); + } +} diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 7d8b2a8a051..01e53626359 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -1,5 +1,9 @@ import { randomUUID } from "node:crypto"; import type { loadConfig } from "../config/config.js"; +import { + logExtensionHostPluginDiagnostics, + resolveExtensionHostGatewayMethods, +} from "../extension-host/gateway-methods.js"; import { loadOpenClawPlugins } from "../plugins/loader.js"; import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; @@ -187,25 +191,13 @@ export function loadGatewayPlugins(params: { subagent: createGatewaySubagentRuntime(), }, }); - const pluginMethods = Object.keys(pluginRegistry.gatewayHandlers); - const gatewayMethods = Array.from(new Set([...params.baseMethods, ...pluginMethods])); - if (pluginRegistry.diagnostics.length > 0) { - for (const diag of pluginRegistry.diagnostics) { - const details = [ - diag.pluginId ? `plugin=${diag.pluginId}` : null, - diag.source ? `source=${diag.source}` : null, - ] - .filter((entry): entry is string => Boolean(entry)) - .join(", "); - const message = details - ? `[plugins] ${diag.message} (${details})` - : `[plugins] ${diag.message}`; - if (diag.level === "error") { - params.log.error(message); - } else { - params.log.info(message); - } - } - } + const gatewayMethods = resolveExtensionHostGatewayMethods({ + registry: pluginRegistry, + baseMethods: params.baseMethods, + }); + logExtensionHostPluginDiagnostics({ + diagnostics: pluginRegistry.diagnostics, + log: params.log, + }); return { pluginRegistry, gatewayMethods }; } diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 5453ff8fcee..19ca423d47b 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -21,6 +21,7 @@ import { import { formatConfigIssueLines } from "../config/issue-format.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { resolveMainSessionKey } from "../config/sessions.js"; +import { createExtensionHostGatewayExtraHandlers } from "../extension-host/gateway-methods.js"; import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js"; import { ensureControlUiAssetsBuilt, @@ -892,11 +893,13 @@ export async function startGatewayServer( logGateway: log, logHealth, logWsControl, - extraHandlers: { - ...pluginRegistry.gatewayHandlers, - ...execApprovalHandlers, - ...secretsHandlers, - }, + extraHandlers: createExtensionHostGatewayExtraHandlers({ + registry: pluginRegistry, + extraHandlers: { + ...execApprovalHandlers, + ...secretsHandlers, + }, + }), broadcast, context: gatewayRequestContext, });