mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
refactor(plugins): root-owned bundle installation with scoped registry handles (#117587)
* refactor(plugins): root-owned bundle installation with scoped registry handles * refactor(plugins): root-owned bundle installation with scoped registry handles * test(gateway): expect gateway-binding fact in reload rewarm * fix(agents): reuse gateway prepared runtime generations * test(agents): isolate runtime handle fixtures * test(agents): align runtime owner fixtures * test(cron): mock run-owned registry loading
This commit is contained in:
committed by
GitHub
parent
2fc32204b4
commit
6c9b38862d
@@ -1,8 +1,12 @@
|
||||
/** Plugin node-host bridge for loading plugin registry commands and dispatching node capabilities. */
|
||||
import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginNodeHostCommandRegistration } from "../plugins/registry-types.js";
|
||||
import type {
|
||||
PluginNodeHostCommandRegistration,
|
||||
PluginRegistry,
|
||||
} from "../plugins/registry-types.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js";
|
||||
import type {
|
||||
OpenClawPluginNodeHostCommandAvailabilityContext,
|
||||
OpenClawPluginNodeHostCommandIo,
|
||||
@@ -18,16 +22,20 @@ import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
|
||||
*/
|
||||
|
||||
const loadPluginRegistryLoaderModule = createLazyRuntimeModule(
|
||||
() => import("../plugins/runtime/runtime-registry-loader.js"),
|
||||
() => import("../plugins/loader.js"),
|
||||
);
|
||||
let nodeHostPluginRegistry: PluginRegistry | undefined;
|
||||
|
||||
function resolveNodeHostPluginRegistry() {
|
||||
return nodeHostPluginRegistry ?? getActivePluginRegistry() ?? undefined;
|
||||
}
|
||||
|
||||
/** Ensure plugin registry data is loaded before node-host command dispatch. */
|
||||
export async function ensureNodeHostPluginRegistry(params: {
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
(await loadPluginRegistryLoaderModule()).ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
nodeHostPluginRegistry = (await loadPluginRegistryLoaderModule()).loadPluginRegistryHandle({
|
||||
config: params.config,
|
||||
activationSourceConfig: params.config,
|
||||
env: params.env,
|
||||
@@ -43,36 +51,38 @@ export function listRegisteredNodeHostCapsAndCommands(
|
||||
commands: string[];
|
||||
nodePluginTools: NodePluginToolDescriptor[];
|
||||
} {
|
||||
const registry = getActivePluginRegistry();
|
||||
const caps = new Set<string>();
|
||||
const commands = new Set<string>();
|
||||
const nodePluginTools = new Map<string, NodePluginToolDescriptor>();
|
||||
for (const entry of registry?.nodeHostCommands ?? []) {
|
||||
if (entry.command.duplex === true && options.includeDuplex === false) {
|
||||
continue;
|
||||
const registry = resolveNodeHostPluginRegistry();
|
||||
return withPluginRuntimeRegistryScope(registry, () => {
|
||||
const caps = new Set<string>();
|
||||
const commands = new Set<string>();
|
||||
const nodePluginTools = new Map<string, NodePluginToolDescriptor>();
|
||||
for (const entry of registry?.nodeHostCommands ?? []) {
|
||||
if (entry.command.duplex === true && options.includeDuplex === false) {
|
||||
continue;
|
||||
}
|
||||
// Availability belongs to the node-local plugin. Gateway policy still keeps
|
||||
// the command registered so a differently configured remote node can expose it.
|
||||
if (entry.command.isAvailable?.(context) === false) {
|
||||
continue;
|
||||
}
|
||||
if (entry.command.cap) {
|
||||
caps.add(entry.command.cap);
|
||||
}
|
||||
commands.add(entry.command.command);
|
||||
const agentTool = buildNodePluginToolDescriptor(entry);
|
||||
if (agentTool) {
|
||||
nodePluginTools.set(`${agentTool.pluginId}\0${agentTool.name}`, agentTool);
|
||||
}
|
||||
}
|
||||
// Availability belongs to the node-local plugin. Gateway policy still keeps
|
||||
// the command registered so a differently configured remote node can expose it.
|
||||
if (entry.command.isAvailable?.(context) === false) {
|
||||
continue;
|
||||
}
|
||||
if (entry.command.cap) {
|
||||
caps.add(entry.command.cap);
|
||||
}
|
||||
commands.add(entry.command.command);
|
||||
const agentTool = buildNodePluginToolDescriptor(entry);
|
||||
if (agentTool) {
|
||||
nodePluginTools.set(`${agentTool.pluginId}\0${agentTool.name}`, agentTool);
|
||||
}
|
||||
}
|
||||
return {
|
||||
caps: [...caps].toSorted((left, right) => left.localeCompare(right)),
|
||||
commands: [...commands].toSorted((left, right) => left.localeCompare(right)),
|
||||
nodePluginTools: [...nodePluginTools.values()].toSorted(
|
||||
(left, right) =>
|
||||
left.pluginId.localeCompare(right.pluginId) || left.name.localeCompare(right.name),
|
||||
),
|
||||
};
|
||||
return {
|
||||
caps: [...caps].toSorted((left, right) => left.localeCompare(right)),
|
||||
commands: [...commands].toSorted((left, right) => left.localeCompare(right)),
|
||||
nodePluginTools: [...nodePluginTools.values()].toSorted(
|
||||
(left, right) =>
|
||||
left.pluginId.localeCompare(right.pluginId) || left.name.localeCompare(right.name),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Watch plugin-owned availability inputs that can change during this process. */
|
||||
@@ -80,19 +90,24 @@ export function watchRegisteredNodeHostCommandAvailability(
|
||||
context: OpenClawPluginNodeHostCommandAvailabilityContext,
|
||||
onChange: () => void,
|
||||
): () => void {
|
||||
const registry = getActivePluginRegistry();
|
||||
const registry = resolveNodeHostPluginRegistry();
|
||||
const cleanups: Array<() => void> = [];
|
||||
for (const entry of registry?.nodeHostCommands ?? []) {
|
||||
const cleanup = entry.command.watchAvailability?.(context, onChange);
|
||||
if (cleanup) {
|
||||
cleanups.push(cleanup);
|
||||
withPluginRuntimeRegistryScope(registry, () => {
|
||||
for (const entry of registry?.nodeHostCommands ?? []) {
|
||||
const cleanup = entry.command.watchAvailability?.(context, () =>
|
||||
withPluginRuntimeRegistryScope(registry, onChange),
|
||||
);
|
||||
if (cleanup) {
|
||||
cleanups.push(cleanup);
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
for (const cleanup of cleanups.splice(0)) {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
});
|
||||
return () =>
|
||||
withPluginRuntimeRegistryScope(registry, () => {
|
||||
for (const cleanup of cleanups.splice(0)) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeString(value: unknown): string {
|
||||
@@ -144,30 +159,43 @@ export async function invokeRegisteredNodeHostCommand(
|
||||
io?: OpenClawPluginNodeHostCommandIo,
|
||||
context?: OpenClawPluginNodeHostCommandContext,
|
||||
): Promise<string | null> {
|
||||
const registry = getActivePluginRegistry();
|
||||
const registry = resolveNodeHostPluginRegistry();
|
||||
const match = (registry?.nodeHostCommands ?? []).find(
|
||||
(entry) => entry.command.command === command,
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
if (match.command.duplex === true) {
|
||||
if (!io) {
|
||||
throw new Error(`node command requires duplex transport: ${command}`);
|
||||
return await withPluginRuntimeRegistryScope(registry, async () => {
|
||||
if (match.command.duplex === true) {
|
||||
if (!io) {
|
||||
throw new Error(`node command requires duplex transport: ${command}`);
|
||||
}
|
||||
return context
|
||||
? await match.command.handle(paramsJSON, io, context)
|
||||
: await match.command.handle(paramsJSON, io);
|
||||
}
|
||||
return context
|
||||
? await match.command.handle(paramsJSON, io, context)
|
||||
: await match.command.handle(paramsJSON, io);
|
||||
}
|
||||
return context
|
||||
? await match.command.handle(paramsJSON, undefined, context)
|
||||
: await match.command.handle(paramsJSON);
|
||||
? await match.command.handle(paramsJSON, undefined, context)
|
||||
: await match.command.handle(paramsJSON);
|
||||
});
|
||||
}
|
||||
|
||||
export function isRegisteredNodeHostCommandDuplex(command: string): boolean {
|
||||
const registry = getActivePluginRegistry();
|
||||
const registry = resolveNodeHostPluginRegistry();
|
||||
return (
|
||||
(registry?.nodeHostCommands ?? []).find((entry) => entry.command.command === command)?.command
|
||||
.duplex === true
|
||||
);
|
||||
}
|
||||
|
||||
function resetNodeHostPluginRegistry(): void {
|
||||
nodeHostPluginRegistry = undefined;
|
||||
}
|
||||
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.nodeHostPluginTestApi")] = {
|
||||
getNodeHostPluginRegistry: () => nodeHostPluginRegistry,
|
||||
resetNodeHostPluginRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user