Plugins: add host-owned route and gateway storage
This commit is contained in:
parent
4cb3600549
commit
a1c5cbabff
@ -5,12 +5,13 @@ import {
|
||||
logExtensionHostPluginDiagnostics,
|
||||
resolveExtensionHostGatewayMethods,
|
||||
} from "./gateway-methods.js";
|
||||
import { setExtensionHostGatewayHandler } from "./runtime-registry.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();
|
||||
setExtensionHostGatewayHandler({ registry, method: "health", handler: vi.fn() });
|
||||
setExtensionHostGatewayHandler({ registry, method: "plugin.echo", handler: vi.fn() });
|
||||
|
||||
expect(
|
||||
resolveExtensionHostGatewayMethods({
|
||||
@ -26,7 +27,7 @@ describe("createExtensionHostGatewayExtraHandlers", () => {
|
||||
const pluginHandler = vi.fn();
|
||||
const callerHandler = vi.fn();
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayHandlers.demo = pluginHandler;
|
||||
setExtensionHostGatewayHandler({ registry, method: "demo", handler: pluginHandler });
|
||||
|
||||
const handlers = createExtensionHostGatewayExtraHandlers({
|
||||
registry,
|
||||
|
||||
@ -43,6 +43,10 @@ import {
|
||||
resolveExtensionToolRegistration,
|
||||
resolveExtensionTypedHookRegistration,
|
||||
} from "./runtime-registrations.js";
|
||||
import {
|
||||
getExtensionHostGatewayHandlers,
|
||||
listExtensionHostHttpRoutes,
|
||||
} from "./runtime-registry.js";
|
||||
|
||||
export type PluginTypedHookPolicy = {
|
||||
allowPromptInjection?: boolean;
|
||||
@ -115,7 +119,7 @@ export function createExtensionHostPluginRegistrationActions(params: {
|
||||
handler: GatewayRequestHandler,
|
||||
) => {
|
||||
const result = resolveExtensionGatewayMethodRegistration({
|
||||
existing: registry.gatewayHandlers,
|
||||
existing: { ...getExtensionHostGatewayHandlers(registry) },
|
||||
coreGatewayMethods,
|
||||
method,
|
||||
handler,
|
||||
@ -140,7 +144,7 @@ export function createExtensionHostPluginRegistrationActions(params: {
|
||||
|
||||
const registerHttpRoute = (record: PluginRecord, route: OpenClawPluginHttpRouteParams) => {
|
||||
const result = resolveExtensionHttpRouteRegistration({
|
||||
existing: registry.httpRoutes,
|
||||
existing: [...listExtensionHostHttpRoutes(registry)],
|
||||
ownerPluginId: record.id,
|
||||
ownerSource: record.source,
|
||||
route,
|
||||
|
||||
@ -24,6 +24,11 @@ import type {
|
||||
ExtensionHostServiceRegistration,
|
||||
ExtensionHostToolRegistration,
|
||||
} from "./runtime-registrations.js";
|
||||
import {
|
||||
addExtensionHostHttpRoute,
|
||||
replaceExtensionHostHttpRoute,
|
||||
setExtensionHostGatewayHandler,
|
||||
} from "./runtime-registry.js";
|
||||
|
||||
export function addExtensionGatewayMethodRegistration(params: {
|
||||
registry: PluginRegistry;
|
||||
@ -31,7 +36,11 @@ export function addExtensionGatewayMethodRegistration(params: {
|
||||
method: string;
|
||||
handler: GatewayRequestHandler;
|
||||
}): void {
|
||||
params.registry.gatewayHandlers[params.method] = params.handler;
|
||||
setExtensionHostGatewayHandler({
|
||||
registry: params.registry,
|
||||
method: params.method,
|
||||
handler: params.handler,
|
||||
});
|
||||
params.record.gatewayMethods.push(params.method);
|
||||
}
|
||||
|
||||
@ -46,12 +55,16 @@ export function addExtensionHttpRouteRegistration(params: {
|
||||
if (params.existingIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
params.registry.httpRoutes[params.existingIndex] = params.entry as PluginHttpRouteRegistration;
|
||||
replaceExtensionHostHttpRoute({
|
||||
registry: params.registry,
|
||||
index: params.existingIndex,
|
||||
entry: params.entry as PluginHttpRouteRegistration,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
params.record.httpRoutes += 1;
|
||||
params.registry.httpRoutes.push(params.entry as PluginHttpRouteRegistration);
|
||||
addExtensionHostHttpRoute(params.registry, params.entry as PluginHttpRouteRegistration);
|
||||
}
|
||||
|
||||
export function addExtensionChannelRegistration(params: {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import {
|
||||
addExtensionHostHttpRoute,
|
||||
getExtensionHostGatewayHandlers,
|
||||
hasExtensionHostRuntimeEntries,
|
||||
listExtensionHostCliRegistrations,
|
||||
@ -8,6 +9,9 @@ import {
|
||||
listExtensionHostProviderRegistrations,
|
||||
listExtensionHostServiceRegistrations,
|
||||
listExtensionHostToolRegistrations,
|
||||
removeExtensionHostHttpRoute,
|
||||
replaceExtensionHostHttpRoute,
|
||||
setExtensionHostGatewayHandler,
|
||||
} from "./runtime-registry.js";
|
||||
|
||||
describe("extension host runtime registry accessors", () => {
|
||||
@ -25,7 +29,7 @@ describe("extension host runtime registry accessors", () => {
|
||||
expect(hasExtensionHostRuntimeEntries(providerRegistry)).toBe(true);
|
||||
|
||||
const routeRegistry = createEmptyPluginRegistry();
|
||||
routeRegistry.httpRoutes.push({
|
||||
addExtensionHostHttpRoute(routeRegistry, {
|
||||
path: "/plugins/demo",
|
||||
handler: vi.fn(),
|
||||
auth: "plugin",
|
||||
@ -36,7 +40,11 @@ describe("extension host runtime registry accessors", () => {
|
||||
expect(hasExtensionHostRuntimeEntries(routeRegistry)).toBe(true);
|
||||
|
||||
const gatewayRegistry = createEmptyPluginRegistry();
|
||||
gatewayRegistry.gatewayHandlers["demo.echo"] = vi.fn();
|
||||
setExtensionHostGatewayHandler({
|
||||
registry: gatewayRegistry,
|
||||
method: "demo.echo",
|
||||
handler: vi.fn(),
|
||||
});
|
||||
expect(hasExtensionHostRuntimeEntries(gatewayRegistry)).toBe(true);
|
||||
});
|
||||
|
||||
@ -80,7 +88,7 @@ describe("extension host runtime registry accessors", () => {
|
||||
commands: ["demo"],
|
||||
register: () => undefined,
|
||||
});
|
||||
registry.httpRoutes.push({
|
||||
addExtensionHostHttpRoute(registry, {
|
||||
path: "/plugins/demo",
|
||||
handler: vi.fn(),
|
||||
auth: "plugin",
|
||||
@ -88,12 +96,49 @@ describe("extension host runtime registry accessors", () => {
|
||||
pluginId: "route-demo",
|
||||
source: "test",
|
||||
});
|
||||
registry.gatewayHandlers["demo.echo"] = vi.fn();
|
||||
const handler = vi.fn();
|
||||
setExtensionHostGatewayHandler({
|
||||
registry,
|
||||
method: "demo.echo",
|
||||
handler,
|
||||
});
|
||||
|
||||
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);
|
||||
expect(listExtensionHostHttpRoutes(registry)).toEqual(registry.httpRoutes);
|
||||
expect(getExtensionHostGatewayHandlers(registry)).toEqual(registry.gatewayHandlers);
|
||||
expect(getExtensionHostGatewayHandlers(registry)["demo.echo"]).toBe(handler);
|
||||
});
|
||||
|
||||
it("keeps legacy route and gateway mirrors synchronized with host-owned state", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const firstHandler = vi.fn();
|
||||
const secondHandler = vi.fn();
|
||||
const entry = {
|
||||
path: "/plugins/demo",
|
||||
handler: firstHandler,
|
||||
auth: "plugin" as const,
|
||||
match: "exact" as const,
|
||||
pluginId: "route-demo",
|
||||
source: "test",
|
||||
};
|
||||
|
||||
addExtensionHostHttpRoute(registry, entry);
|
||||
setExtensionHostGatewayHandler({
|
||||
registry,
|
||||
method: "demo.echo",
|
||||
handler: firstHandler,
|
||||
});
|
||||
replaceExtensionHostHttpRoute({
|
||||
registry,
|
||||
index: 0,
|
||||
entry: { ...entry, handler: secondHandler },
|
||||
});
|
||||
removeExtensionHostHttpRoute(registry, entry);
|
||||
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
expect(registry.httpRoutes[0]?.handler).toBe(secondHandler);
|
||||
expect(getExtensionHostGatewayHandlers(registry)).toEqual(registry.gatewayHandlers);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,56 @@ 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({});
|
||||
const EXTENSION_HOST_RUNTIME_REGISTRY_STATE = Symbol.for("openclaw.extensionHostRuntimeRegistry");
|
||||
|
||||
type ExtensionHostRuntimeRegistryState = {
|
||||
httpRoutes: PluginHttpRouteRegistration[];
|
||||
legacyHttpRoutes: PluginHttpRouteRegistration[];
|
||||
gatewayHandlers: GatewayRequestHandlers;
|
||||
legacyGatewayHandlers: GatewayRequestHandlers;
|
||||
};
|
||||
|
||||
type RuntimeRegistryBackedPluginRegistry = Pick<
|
||||
PluginRegistry,
|
||||
"httpRoutes" | "gatewayHandlers"
|
||||
> & {
|
||||
[EXTENSION_HOST_RUNTIME_REGISTRY_STATE]?: ExtensionHostRuntimeRegistryState;
|
||||
};
|
||||
|
||||
function ensureExtensionHostRuntimeRegistryState(
|
||||
registry: RuntimeRegistryBackedPluginRegistry,
|
||||
): ExtensionHostRuntimeRegistryState {
|
||||
if (registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE]) {
|
||||
return registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE];
|
||||
}
|
||||
|
||||
const legacyHttpRoutes = registry.httpRoutes ?? [];
|
||||
registry.httpRoutes = legacyHttpRoutes;
|
||||
const legacyGatewayHandlers = registry.gatewayHandlers ?? {};
|
||||
registry.gatewayHandlers = legacyGatewayHandlers;
|
||||
|
||||
const state: ExtensionHostRuntimeRegistryState = {
|
||||
httpRoutes: [...legacyHttpRoutes],
|
||||
legacyHttpRoutes,
|
||||
gatewayHandlers: { ...legacyGatewayHandlers },
|
||||
legacyGatewayHandlers,
|
||||
};
|
||||
registry[EXTENSION_HOST_RUNTIME_REGISTRY_STATE] = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
function syncLegacyHttpRoutes(state: ExtensionHostRuntimeRegistryState): void {
|
||||
state.legacyHttpRoutes.splice(0, state.legacyHttpRoutes.length, ...state.httpRoutes);
|
||||
}
|
||||
|
||||
function syncLegacyGatewayHandlers(state: ExtensionHostRuntimeRegistryState): void {
|
||||
for (const key of Object.keys(state.legacyGatewayHandlers)) {
|
||||
if (!(key in state.gatewayHandlers)) {
|
||||
delete state.legacyGatewayHandlers[key];
|
||||
}
|
||||
}
|
||||
Object.assign(state.legacyGatewayHandlers, state.gatewayHandlers);
|
||||
}
|
||||
|
||||
export function hasExtensionHostRuntimeEntries(
|
||||
registry:
|
||||
@ -42,8 +92,8 @@ export function hasExtensionHostRuntimeEntries(
|
||||
registry.channels.length > 0 ||
|
||||
registry.tools.length > 0 ||
|
||||
registry.providers.length > 0 ||
|
||||
Object.keys(registry.gatewayHandlers).length > 0 ||
|
||||
registry.httpRoutes.length > 0 ||
|
||||
Object.keys(getExtensionHostGatewayHandlers(registry)).length > 0 ||
|
||||
listExtensionHostHttpRoutes(registry).length > 0 ||
|
||||
registry.cliRegistrars.length > 0 ||
|
||||
registry.services.length > 0 ||
|
||||
registry.commands.length > 0 ||
|
||||
@ -77,13 +127,71 @@ export function listExtensionHostCliRegistrations(
|
||||
}
|
||||
|
||||
export function listExtensionHostHttpRoutes(
|
||||
registry: Pick<PluginRegistry, "httpRoutes"> | null | undefined,
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers"> | null | undefined,
|
||||
): readonly PluginHttpRouteRegistration[] {
|
||||
return registry?.httpRoutes ?? EMPTY_HTTP_ROUTES;
|
||||
if (!registry) {
|
||||
return EMPTY_HTTP_ROUTES;
|
||||
}
|
||||
return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry)
|
||||
.httpRoutes;
|
||||
}
|
||||
|
||||
export function getExtensionHostGatewayHandlers(
|
||||
registry: Pick<PluginRegistry, "gatewayHandlers"> | null | undefined,
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers"> | null | undefined,
|
||||
): Readonly<GatewayRequestHandlers> {
|
||||
return registry?.gatewayHandlers ?? EMPTY_GATEWAY_HANDLERS;
|
||||
if (!registry) {
|
||||
return EMPTY_GATEWAY_HANDLERS;
|
||||
}
|
||||
return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry)
|
||||
.gatewayHandlers;
|
||||
}
|
||||
|
||||
export function addExtensionHostHttpRoute(
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers">,
|
||||
entry: PluginHttpRouteRegistration,
|
||||
): void {
|
||||
const state = ensureExtensionHostRuntimeRegistryState(
|
||||
registry as RuntimeRegistryBackedPluginRegistry,
|
||||
);
|
||||
state.httpRoutes.push(entry);
|
||||
syncLegacyHttpRoutes(state);
|
||||
}
|
||||
|
||||
export function replaceExtensionHostHttpRoute(params: {
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers">;
|
||||
index: number;
|
||||
entry: PluginHttpRouteRegistration;
|
||||
}): void {
|
||||
const state = ensureExtensionHostRuntimeRegistryState(
|
||||
params.registry as RuntimeRegistryBackedPluginRegistry,
|
||||
);
|
||||
state.httpRoutes[params.index] = params.entry;
|
||||
syncLegacyHttpRoutes(state);
|
||||
}
|
||||
|
||||
export function removeExtensionHostHttpRoute(
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers">,
|
||||
entry: PluginHttpRouteRegistration,
|
||||
): void {
|
||||
const state = ensureExtensionHostRuntimeRegistryState(
|
||||
registry as RuntimeRegistryBackedPluginRegistry,
|
||||
);
|
||||
const index = state.httpRoutes.indexOf(entry);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
state.httpRoutes.splice(index, 1);
|
||||
syncLegacyHttpRoutes(state);
|
||||
}
|
||||
|
||||
export function setExtensionHostGatewayHandler(params: {
|
||||
registry: Pick<PluginRegistry, "httpRoutes" | "gatewayHandlers">;
|
||||
method: string;
|
||||
handler: GatewayRequestHandlers[string];
|
||||
}): void {
|
||||
const state = ensureExtensionHostRuntimeRegistryState(
|
||||
params.registry as RuntimeRegistryBackedPluginRegistry,
|
||||
);
|
||||
state.gatewayHandlers[params.method] = params.handler;
|
||||
syncLegacyGatewayHandlers(state);
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { requireActiveExtensionHostRegistry } from "../extension-host/active-registry.js";
|
||||
import {
|
||||
addExtensionHostHttpRoute,
|
||||
listExtensionHostHttpRoutes,
|
||||
removeExtensionHostHttpRoute,
|
||||
replaceExtensionHostHttpRoute,
|
||||
} from "../extension-host/runtime-registry.js";
|
||||
import { normalizePluginHttpPath } from "./http-path.js";
|
||||
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
|
||||
import type { PluginHttpRouteRegistration, PluginRegistry } from "./registry.js";
|
||||
@ -23,8 +29,7 @@ export function registerPluginHttpRoute(params: {
|
||||
registry?: PluginRegistry;
|
||||
}): () => void {
|
||||
const registry = params.registry ?? requireActiveExtensionHostRegistry();
|
||||
const routes = registry.httpRoutes ?? [];
|
||||
registry.httpRoutes = routes;
|
||||
const routes = listExtensionHostHttpRoutes(registry);
|
||||
|
||||
const normalizedPath = normalizePluginHttpPath(params.path, params.fallbackPath);
|
||||
const suffix = params.accountId ? ` for account "${params.accountId}"` : "";
|
||||
@ -70,7 +75,6 @@ export function registerPluginHttpRoute(params: {
|
||||
params.log?.(
|
||||
`plugin: replacing stale webhook path ${normalizedPath} (${routeMatch})${suffix}${pluginHint}`,
|
||||
);
|
||||
routes.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
const entry: PluginHttpRouteRegistration = {
|
||||
@ -81,12 +85,17 @@ export function registerPluginHttpRoute(params: {
|
||||
pluginId: params.pluginId,
|
||||
source: params.source,
|
||||
};
|
||||
routes.push(entry);
|
||||
if (existingIndex >= 0) {
|
||||
replaceExtensionHostHttpRoute({
|
||||
registry,
|
||||
index: existingIndex,
|
||||
entry,
|
||||
});
|
||||
} else {
|
||||
addExtensionHostHttpRoute(registry, entry);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const index = routes.indexOf(entry);
|
||||
if (index >= 0) {
|
||||
routes.splice(index, 1);
|
||||
}
|
||||
removeExtensionHostHttpRoute(registry, entry);
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user