mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 00:42:24 +00:00
Plugins: add runtime registry read surface
This commit is contained in:
67
src/cli/plugin-registry.test.ts
Normal file
67
src/cli/plugin-registry.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
|
||||
const loadOpenClawPluginsMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistryMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: () => "/tmp/workspace",
|
||||
resolveDefaultAgentId: () => "default-agent",
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("../logging.js", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: loadOpenClawPluginsMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: getActivePluginRegistryMock,
|
||||
}));
|
||||
|
||||
describe("ensurePluginRegistryLoaded", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("skips plugin loading when a provider-only registry is already active", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({
|
||||
pluginId: "provider-demo",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "provider-demo",
|
||||
label: "Provider Demo",
|
||||
auth: [],
|
||||
},
|
||||
});
|
||||
getActivePluginRegistryMock.mockReturnValue(registry);
|
||||
|
||||
const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js");
|
||||
ensurePluginRegistryLoaded();
|
||||
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads plugins once when the active registry is empty", async () => {
|
||||
getActivePluginRegistryMock.mockReturnValue(createEmptyPluginRegistry());
|
||||
|
||||
const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js");
|
||||
ensurePluginRegistryLoaded();
|
||||
ensurePluginRegistryLoaded();
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { hasExtensionHostRuntimeEntries } from "../extension-host/runtime-registry.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
@@ -14,11 +15,8 @@ export function ensurePluginRegistryLoaded(): void {
|
||||
}
|
||||
const active = getActivePluginRegistry();
|
||||
// Tests (and callers) can pre-seed a registry (e.g. `test/setup.ts`); avoid
|
||||
// doing an expensive load when we already have plugins/channels/tools.
|
||||
if (
|
||||
active &&
|
||||
(active.plugins.length > 0 || active.channels.length > 0 || active.tools.length > 0)
|
||||
) {
|
||||
// doing an expensive load when we already have runtime entries.
|
||||
if (hasExtensionHostRuntimeEntries(active)) {
|
||||
pluginRegistryLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import { listExtensionHostCliRegistrations } from "./runtime-registry.js";
|
||||
|
||||
export function registerExtensionHostCliCommands(params: {
|
||||
program: Command;
|
||||
@@ -12,7 +13,7 @@ export function registerExtensionHostCliCommands(params: {
|
||||
}): void {
|
||||
const existingCommands = new Set(params.program.commands.map((cmd) => cmd.name()));
|
||||
|
||||
for (const entry of params.registry.cliRegistrars) {
|
||||
for (const entry of listExtensionHostCliRegistrations(params.registry)) {
|
||||
if (entry.commands.length > 0) {
|
||||
const overlaps = entry.commands.filter((command) => existingCommands.has(command));
|
||||
if (overlaps.length > 0) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginDiagnostic } from "../plugins/types.js";
|
||||
import { getExtensionHostGatewayHandlers } from "./runtime-registry.js";
|
||||
|
||||
export function resolveExtensionHostGatewayMethods(params: {
|
||||
registry: PluginRegistry;
|
||||
baseMethods: string[];
|
||||
}): string[] {
|
||||
const pluginMethods = Object.keys(params.registry.gatewayHandlers);
|
||||
const pluginMethods = Object.keys(getExtensionHostGatewayHandlers(params.registry));
|
||||
return Array.from(new Set([...params.baseMethods, ...pluginMethods]));
|
||||
}
|
||||
|
||||
@@ -14,8 +15,9 @@ export function createExtensionHostGatewayExtraHandlers(params: {
|
||||
registry: PluginRegistry;
|
||||
extraHandlers?: GatewayRequestHandlers;
|
||||
}): GatewayRequestHandlers {
|
||||
const pluginHandlers = getExtensionHostGatewayHandlers(params.registry);
|
||||
return {
|
||||
...params.registry.gatewayHandlers,
|
||||
...pluginHandlers,
|
||||
...params.extraHandlers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { listExtensionHostProviderRegistrations } from "./runtime-registry.js";
|
||||
|
||||
export function resolveExtensionHostProviders(params: {
|
||||
registry: Pick<PluginRegistry, "providers">;
|
||||
}): ProviderPlugin[] {
|
||||
return params.registry.providers.map((entry) => ({
|
||||
return listExtensionHostProviderRegistrations(params.registry).map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
}));
|
||||
|
||||
99
src/extension-host/runtime-registry.test.ts
Normal file
99
src/extension-host/runtime-registry.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import {
|
||||
getExtensionHostGatewayHandlers,
|
||||
hasExtensionHostRuntimeEntries,
|
||||
listExtensionHostCliRegistrations,
|
||||
listExtensionHostHttpRoutes,
|
||||
listExtensionHostProviderRegistrations,
|
||||
listExtensionHostServiceRegistrations,
|
||||
listExtensionHostToolRegistrations,
|
||||
} from "./runtime-registry.js";
|
||||
|
||||
describe("extension host runtime registry accessors", () => {
|
||||
it("detects runtime entries across non-tool surfaces", () => {
|
||||
const providerRegistry = createEmptyPluginRegistry();
|
||||
providerRegistry.providers.push({
|
||||
pluginId: "provider-demo",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "provider-demo",
|
||||
label: "Provider Demo",
|
||||
auth: [],
|
||||
},
|
||||
});
|
||||
expect(hasExtensionHostRuntimeEntries(providerRegistry)).toBe(true);
|
||||
|
||||
const routeRegistry = createEmptyPluginRegistry();
|
||||
routeRegistry.httpRoutes.push({
|
||||
path: "/plugins/demo",
|
||||
handler: vi.fn(),
|
||||
auth: "plugin",
|
||||
match: "exact",
|
||||
pluginId: "route-demo",
|
||||
source: "test",
|
||||
});
|
||||
expect(hasExtensionHostRuntimeEntries(routeRegistry)).toBe(true);
|
||||
|
||||
const gatewayRegistry = createEmptyPluginRegistry();
|
||||
gatewayRegistry.gatewayHandlers["demo.echo"] = vi.fn();
|
||||
expect(hasExtensionHostRuntimeEntries(gatewayRegistry)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns stable empty views for missing registries", () => {
|
||||
expect(hasExtensionHostRuntimeEntries(null)).toBe(false);
|
||||
expect(listExtensionHostProviderRegistrations(null)).toEqual([]);
|
||||
expect(listExtensionHostToolRegistrations(null)).toEqual([]);
|
||||
expect(listExtensionHostServiceRegistrations(null)).toEqual([]);
|
||||
expect(listExtensionHostCliRegistrations(null)).toEqual([]);
|
||||
expect(listExtensionHostHttpRoutes(null)).toEqual([]);
|
||||
expect(getExtensionHostGatewayHandlers(null)).toEqual({});
|
||||
});
|
||||
|
||||
it("projects existing registry collections without copying them", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.tools.push({
|
||||
pluginId: "tool-demo",
|
||||
optional: false,
|
||||
source: "test",
|
||||
names: ["tool_demo"],
|
||||
factory: () => ({
|
||||
name: "tool_demo",
|
||||
description: "tool demo",
|
||||
parameters: { type: "object", properties: {} },
|
||||
async execute() {
|
||||
return { content: [{ type: "text", text: "ok" }] };
|
||||
},
|
||||
}),
|
||||
});
|
||||
registry.services.push({
|
||||
pluginId: "svc-demo",
|
||||
source: "test",
|
||||
service: {
|
||||
id: "svc-demo",
|
||||
start: () => undefined,
|
||||
},
|
||||
});
|
||||
registry.cliRegistrars.push({
|
||||
pluginId: "cli-demo",
|
||||
source: "test",
|
||||
commands: ["demo"],
|
||||
register: () => undefined,
|
||||
});
|
||||
registry.httpRoutes.push({
|
||||
path: "/plugins/demo",
|
||||
handler: vi.fn(),
|
||||
auth: "plugin",
|
||||
match: "exact",
|
||||
pluginId: "route-demo",
|
||||
source: "test",
|
||||
});
|
||||
registry.gatewayHandlers["demo.echo"] = vi.fn();
|
||||
|
||||
expect(listExtensionHostToolRegistrations(registry)).toBe(registry.tools);
|
||||
expect(listExtensionHostServiceRegistrations(registry)).toBe(registry.services);
|
||||
expect(listExtensionHostCliRegistrations(registry)).toBe(registry.cliRegistrars);
|
||||
expect(listExtensionHostHttpRoutes(registry)).toBe(registry.httpRoutes);
|
||||
expect(getExtensionHostGatewayHandlers(registry)).toBe(registry.gatewayHandlers);
|
||||
});
|
||||
});
|
||||
89
src/extension-host/runtime-registry.ts
Normal file
89
src/extension-host/runtime-registry.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js";
|
||||
import type {
|
||||
PluginCliRegistration,
|
||||
PluginHttpRouteRegistration,
|
||||
PluginProviderRegistration,
|
||||
PluginRegistry,
|
||||
PluginServiceRegistration,
|
||||
PluginToolRegistration,
|
||||
} from "../plugins/registry.js";
|
||||
|
||||
const EMPTY_PROVIDERS: readonly PluginProviderRegistration[] = [];
|
||||
const EMPTY_TOOLS: readonly PluginToolRegistration[] = [];
|
||||
const EMPTY_SERVICES: readonly PluginServiceRegistration[] = [];
|
||||
const EMPTY_CLI_REGISTRARS: readonly PluginCliRegistration[] = [];
|
||||
const EMPTY_HTTP_ROUTES: readonly PluginHttpRouteRegistration[] = [];
|
||||
const EMPTY_GATEWAY_HANDLERS: Readonly<GatewayRequestHandlers> = Object.freeze({});
|
||||
|
||||
export function hasExtensionHostRuntimeEntries(
|
||||
registry:
|
||||
| Pick<
|
||||
PluginRegistry,
|
||||
| "plugins"
|
||||
| "channels"
|
||||
| "tools"
|
||||
| "providers"
|
||||
| "gatewayHandlers"
|
||||
| "httpRoutes"
|
||||
| "cliRegistrars"
|
||||
| "services"
|
||||
| "commands"
|
||||
| "hooks"
|
||||
| "typedHooks"
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
): boolean {
|
||||
if (!registry) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
registry.plugins.length > 0 ||
|
||||
registry.channels.length > 0 ||
|
||||
registry.tools.length > 0 ||
|
||||
registry.providers.length > 0 ||
|
||||
Object.keys(registry.gatewayHandlers).length > 0 ||
|
||||
registry.httpRoutes.length > 0 ||
|
||||
registry.cliRegistrars.length > 0 ||
|
||||
registry.services.length > 0 ||
|
||||
registry.commands.length > 0 ||
|
||||
registry.hooks.length > 0 ||
|
||||
registry.typedHooks.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function listExtensionHostProviderRegistrations(
|
||||
registry: Pick<PluginRegistry, "providers"> | null | undefined,
|
||||
): readonly PluginProviderRegistration[] {
|
||||
return registry?.providers ?? EMPTY_PROVIDERS;
|
||||
}
|
||||
|
||||
export function listExtensionHostToolRegistrations(
|
||||
registry: Pick<PluginRegistry, "tools"> | null | undefined,
|
||||
): readonly PluginToolRegistration[] {
|
||||
return registry?.tools ?? EMPTY_TOOLS;
|
||||
}
|
||||
|
||||
export function listExtensionHostServiceRegistrations(
|
||||
registry: Pick<PluginRegistry, "services"> | null | undefined,
|
||||
): readonly PluginServiceRegistration[] {
|
||||
return registry?.services ?? EMPTY_SERVICES;
|
||||
}
|
||||
|
||||
export function listExtensionHostCliRegistrations(
|
||||
registry: Pick<PluginRegistry, "cliRegistrars"> | null | undefined,
|
||||
): readonly PluginCliRegistration[] {
|
||||
return registry?.cliRegistrars ?? EMPTY_CLI_REGISTRARS;
|
||||
}
|
||||
|
||||
export function listExtensionHostHttpRoutes(
|
||||
registry: Pick<PluginRegistry, "httpRoutes"> | null | undefined,
|
||||
): readonly PluginHttpRouteRegistration[] {
|
||||
return registry?.httpRoutes ?? EMPTY_HTTP_ROUTES;
|
||||
}
|
||||
|
||||
export function getExtensionHostGatewayHandlers(
|
||||
registry: Pick<PluginRegistry, "gatewayHandlers"> | null | undefined,
|
||||
): Readonly<GatewayRequestHandlers> {
|
||||
return registry?.gatewayHandlers ?? EMPTY_GATEWAY_HANDLERS;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { STATE_DIR } from "../config/paths.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { OpenClawPluginServiceContext, PluginLogger } from "../plugins/types.js";
|
||||
import { listExtensionHostServiceRegistrations } from "./runtime-registry.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
@@ -45,7 +46,7 @@ export async function startExtensionHostServices(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
|
||||
for (const entry of params.registry.services) {
|
||||
for (const entry of listExtensionHostServiceRegistrations(params.registry)) {
|
||||
const service = entry.service;
|
||||
try {
|
||||
await service.start(serviceContext);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { OpenClawPluginToolContext } from "../plugins/types.js";
|
||||
import { listExtensionHostToolRegistrations } from "./runtime-registry.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
@@ -55,7 +56,7 @@ export function resolveExtensionHostPluginTools(params: {
|
||||
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
||||
const blockedPlugins = new Set<string>();
|
||||
|
||||
for (const entry of params.registry.tools) {
|
||||
for (const entry of listExtensionHostToolRegistrations(params.registry)) {
|
||||
if (blockedPlugins.has(entry.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { listExtensionHostHttpRoutes } from "../../extension-host/runtime-registry.js";
|
||||
import type { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { withPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js";
|
||||
@@ -65,7 +66,7 @@ export function createGatewayPluginRequestHandler(params: {
|
||||
}): PluginHttpRequestHandler {
|
||||
const { registry, log } = params;
|
||||
return async (req, res, providedPathContext, dispatchContext) => {
|
||||
const routes = registry.httpRoutes ?? [];
|
||||
const routes = listExtensionHostHttpRoutes(registry);
|
||||
if (routes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { listExtensionHostHttpRoutes } from "../../../extension-host/runtime-registry.js";
|
||||
import type { PluginRegistry } from "../../../plugins/registry.js";
|
||||
import { canonicalizePathVariant } from "../../security-path.js";
|
||||
import {
|
||||
@@ -23,7 +24,7 @@ export function findMatchingPluginHttpRoutes(
|
||||
registry: PluginRegistry,
|
||||
context: PluginRoutePathContext,
|
||||
): PluginHttpRouteEntry[] {
|
||||
const routes = registry.httpRoutes ?? [];
|
||||
const routes = listExtensionHostHttpRoutes(registry);
|
||||
if (routes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user