mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
refactor: move browser runtime seams behind plugin metadata
This commit is contained in:
@@ -24,6 +24,9 @@ export type BuildPluginApiParams = {
|
||||
| "registerChannel"
|
||||
| "registerGatewayMethod"
|
||||
| "registerCli"
|
||||
| "registerReload"
|
||||
| "registerNodeHostCommand"
|
||||
| "registerSecurityAuditCollector"
|
||||
| "registerService"
|
||||
| "registerConfigMigration"
|
||||
| "registerAutoEnableProbe"
|
||||
@@ -55,6 +58,10 @@ const noopRegisterHttpRoute: OpenClawPluginApi["registerHttpRoute"] = () => {};
|
||||
const noopRegisterChannel: OpenClawPluginApi["registerChannel"] = () => {};
|
||||
const noopRegisterGatewayMethod: OpenClawPluginApi["registerGatewayMethod"] = () => {};
|
||||
const noopRegisterCli: OpenClawPluginApi["registerCli"] = () => {};
|
||||
const noopRegisterReload: OpenClawPluginApi["registerReload"] = () => {};
|
||||
const noopRegisterNodeHostCommand: OpenClawPluginApi["registerNodeHostCommand"] = () => {};
|
||||
const noopRegisterSecurityAuditCollector: OpenClawPluginApi["registerSecurityAuditCollector"] =
|
||||
() => {};
|
||||
const noopRegisterService: OpenClawPluginApi["registerService"] = () => {};
|
||||
const noopRegisterConfigMigration: OpenClawPluginApi["registerConfigMigration"] = () => {};
|
||||
const noopRegisterAutoEnableProbe: OpenClawPluginApi["registerAutoEnableProbe"] = () => {};
|
||||
@@ -104,6 +111,10 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
|
||||
registerChannel: handlers.registerChannel ?? noopRegisterChannel,
|
||||
registerGatewayMethod: handlers.registerGatewayMethod ?? noopRegisterGatewayMethod,
|
||||
registerCli: handlers.registerCli ?? noopRegisterCli,
|
||||
registerReload: handlers.registerReload ?? noopRegisterReload,
|
||||
registerNodeHostCommand: handlers.registerNodeHostCommand ?? noopRegisterNodeHostCommand,
|
||||
registerSecurityAuditCollector:
|
||||
handlers.registerSecurityAuditCollector ?? noopRegisterSecurityAuditCollector,
|
||||
registerService: handlers.registerService ?? noopRegisterService,
|
||||
registerConfigMigration: handlers.registerConfigMigration ?? noopRegisterConfigMigration,
|
||||
registerAutoEnableProbe: handlers.registerAutoEnableProbe ?? noopRegisterAutoEnableProbe,
|
||||
|
||||
@@ -1093,7 +1093,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
},
|
||||
});
|
||||
|
||||
const { registry, createApi } = createPluginRegistry({
|
||||
const {
|
||||
registry,
|
||||
createApi,
|
||||
registerReload,
|
||||
registerNodeHostCommand,
|
||||
registerSecurityAuditCollector,
|
||||
} = createPluginRegistry({
|
||||
logger,
|
||||
runtime,
|
||||
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
|
||||
@@ -1536,6 +1542,18 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
}
|
||||
}
|
||||
|
||||
if (registrationMode === "full") {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (validateOnly) {
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
|
||||
@@ -22,6 +22,9 @@ export function createEmptyPluginRegistry(): PluginRegistry {
|
||||
gatewayMethodScopes: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
reloads: [],
|
||||
nodeHostCommands: [],
|
||||
securityAuditCollectors: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
|
||||
@@ -9,6 +9,11 @@ import type {
|
||||
} from "../gateway/server-methods/types.js";
|
||||
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import {
|
||||
NODE_EXEC_APPROVALS_COMMANDS,
|
||||
NODE_SYSTEM_NOTIFY_COMMAND,
|
||||
NODE_SYSTEM_RUN_COMMANDS,
|
||||
} from "../infra/node-commands.js";
|
||||
import { normalizePluginGatewayMethodScope } from "../shared/gateway-method-policy.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
@@ -52,6 +57,9 @@ import type {
|
||||
OpenClawPluginHttpRouteHandler,
|
||||
OpenClawPluginHttpRouteParams,
|
||||
OpenClawPluginHookOptions,
|
||||
OpenClawPluginNodeHostCommand,
|
||||
OpenClawPluginReloadRegistration,
|
||||
OpenClawPluginSecurityAuditCollector,
|
||||
MediaUnderstandingProviderPlugin,
|
||||
ProviderPlugin,
|
||||
RealtimeVoiceProviderPlugin,
|
||||
@@ -172,6 +180,30 @@ export type PluginServiceRegistration = {
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginReloadRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
registration: OpenClawPluginReloadRegistration;
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginNodeHostCommandRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
command: OpenClawPluginNodeHostCommand;
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginSecurityAuditCollectorRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
collector: OpenClawPluginSecurityAuditCollector;
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
};
|
||||
|
||||
export type PluginCommandRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
@@ -259,6 +291,9 @@ export type PluginRegistry = {
|
||||
gatewayMethodScopes?: Partial<Record<string, OperatorScope>>;
|
||||
httpRoutes: PluginHttpRouteRegistration[];
|
||||
cliRegistrars: PluginCliRegistration[];
|
||||
reloads?: PluginReloadRegistration[];
|
||||
nodeHostCommands?: PluginNodeHostCommandRegistration[];
|
||||
securityAuditCollectors?: PluginSecurityAuditCollectorRegistration[];
|
||||
services: PluginServiceRegistration[];
|
||||
commands: PluginCommandRegistration[];
|
||||
conversationBindingResolvedHandlers: PluginConversationBindingResolvedHandlerRegistration[];
|
||||
@@ -824,6 +859,104 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
};
|
||||
|
||||
const reservedNodeHostCommands = new Set<string>([
|
||||
...NODE_SYSTEM_RUN_COMMANDS,
|
||||
...NODE_EXEC_APPROVALS_COMMANDS,
|
||||
NODE_SYSTEM_NOTIFY_COMMAND,
|
||||
]);
|
||||
|
||||
const registerReload = (record: PluginRecord, registration: OpenClawPluginReloadRegistration) => {
|
||||
const normalize = (values?: string[]) =>
|
||||
(values ?? []).map((value) => value.trim()).filter(Boolean);
|
||||
const normalized: OpenClawPluginReloadRegistration = {
|
||||
restartPrefixes: normalize(registration.restartPrefixes),
|
||||
hotPrefixes: normalize(registration.hotPrefixes),
|
||||
noopPrefixes: normalize(registration.noopPrefixes),
|
||||
};
|
||||
if (
|
||||
(normalized.restartPrefixes?.length ?? 0) === 0 &&
|
||||
(normalized.hotPrefixes?.length ?? 0) === 0 &&
|
||||
(normalized.noopPrefixes?.length ?? 0) === 0
|
||||
) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "reload registration missing prefixes",
|
||||
});
|
||||
return;
|
||||
}
|
||||
registry.reloads ??= [];
|
||||
registry.reloads.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
registration: normalized,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
const registerNodeHostCommand = (
|
||||
record: PluginRecord,
|
||||
nodeCommand: OpenClawPluginNodeHostCommand,
|
||||
) => {
|
||||
const command = nodeCommand.command.trim();
|
||||
if (!command) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "node host command registration missing command",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (reservedNodeHostCommands.has(command)) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `node host command reserved by core: ${command}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
registry.nodeHostCommands ??= [];
|
||||
const existing = registry.nodeHostCommands.find((entry) => entry.command.command === command);
|
||||
if (existing) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `node host command already registered: ${command} (${existing.pluginId})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
registry.nodeHostCommands.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
command: {
|
||||
...nodeCommand,
|
||||
command,
|
||||
cap: nodeCommand.cap?.trim() || undefined,
|
||||
},
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
const registerSecurityAuditCollector = (
|
||||
record: PluginRecord,
|
||||
collector: OpenClawPluginSecurityAuditCollector,
|
||||
) => {
|
||||
registry.securityAuditCollectors ??= [];
|
||||
registry.securityAuditCollectors.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
collector,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
const registerService = (record: PluginRecord, service: OpenClawPluginService) => {
|
||||
const id = service.id.trim();
|
||||
if (!id) {
|
||||
@@ -1051,6 +1184,10 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registerGatewayMethod: (method, handler, opts) =>
|
||||
registerGatewayMethod(record, method, handler, opts),
|
||||
registerService: (service) => registerService(record, service),
|
||||
registerReload: (registration) => registerReload(record, registration),
|
||||
registerNodeHostCommand: (command) => registerNodeHostCommand(record, command),
|
||||
registerSecurityAuditCollector: (collector) =>
|
||||
registerSecurityAuditCollector(record, collector),
|
||||
registerInteractiveHandler: (registration) => {
|
||||
const result = registerPluginInteractiveHandler(record.id, registration, {
|
||||
pluginName: record.name,
|
||||
@@ -1247,6 +1384,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registerWebSearchProvider,
|
||||
registerGatewayMethod,
|
||||
registerCli,
|
||||
registerReload,
|
||||
registerNodeHostCommand,
|
||||
registerSecurityAuditCollector,
|
||||
registerService,
|
||||
registerCommand,
|
||||
registerHook,
|
||||
|
||||
@@ -33,10 +33,17 @@ function activeRegistrySatisfiesScope(
|
||||
scope: PluginRegistryScope,
|
||||
active: ReturnType<typeof getActivePluginRegistry>,
|
||||
expectedChannelPluginIds: readonly string[],
|
||||
requestedPluginIds: readonly string[],
|
||||
): boolean {
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
if (requestedPluginIds.length > 0) {
|
||||
const activePluginIds = new Set(
|
||||
active.plugins.filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id),
|
||||
);
|
||||
return requestedPluginIds.every((pluginId) => activePluginIds.has(pluginId));
|
||||
}
|
||||
const activeChannelPluginIds = new Set(active.channels.map((entry) => entry.plugin.id));
|
||||
switch (scope) {
|
||||
case "configured-channels":
|
||||
@@ -55,9 +62,13 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
config?: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): void {
|
||||
const scope = options?.scope ?? "all";
|
||||
if (scopeRank(pluginRegistryLoaded) >= scopeRank(scope)) {
|
||||
const requestedPluginIds =
|
||||
options?.onlyPluginIds?.map((pluginId) => pluginId.trim()).filter(Boolean) ?? [];
|
||||
const scopedLoad = requestedPluginIds.length > 0;
|
||||
if (!scopedLoad && scopeRank(pluginRegistryLoaded) >= scopeRank(scope)) {
|
||||
return;
|
||||
}
|
||||
const env = options?.env ?? process.env;
|
||||
@@ -68,8 +79,9 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
resolvedConfig,
|
||||
resolveDefaultAgentId(resolvedConfig),
|
||||
);
|
||||
const expectedChannelPluginIds =
|
||||
scope === "configured-channels"
|
||||
const expectedChannelPluginIds = scopedLoad
|
||||
? requestedPluginIds
|
||||
: scope === "configured-channels"
|
||||
? resolveConfiguredChannelPluginIds({
|
||||
config: resolvedConfig,
|
||||
workspaceDir,
|
||||
@@ -84,10 +96,12 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
: [];
|
||||
const active = getActivePluginRegistry();
|
||||
if (
|
||||
pluginRegistryLoaded === "none" &&
|
||||
activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds)
|
||||
(pluginRegistryLoaded === "none" || scopedLoad) &&
|
||||
activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds, expectedChannelPluginIds)
|
||||
) {
|
||||
pluginRegistryLoaded = scope;
|
||||
if (!scopedLoad) {
|
||||
pluginRegistryLoaded = scope;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const logger: PluginLogger = {
|
||||
@@ -103,11 +117,11 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
workspaceDir,
|
||||
logger,
|
||||
throwOnLoadError: true,
|
||||
...(scope === "configured-channels" || scope === "channels"
|
||||
? { onlyPluginIds: expectedChannelPluginIds }
|
||||
: {}),
|
||||
...(expectedChannelPluginIds.length > 0 ? { onlyPluginIds: expectedChannelPluginIds } : {}),
|
||||
});
|
||||
pluginRegistryLoaded = scope;
|
||||
if (!scopedLoad) {
|
||||
pluginRegistryLoaded = scope;
|
||||
}
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
|
||||
@@ -53,6 +53,7 @@ import type {
|
||||
RuntimeWebFetchMetadata,
|
||||
RuntimeWebSearchMetadata,
|
||||
} from "../secrets/runtime-web-tools.types.js";
|
||||
import type { SecurityAuditFinding } from "../security/audit.js";
|
||||
import type {
|
||||
SpeechDirectiveTokenParseContext,
|
||||
SpeechDirectiveTokenParseResult,
|
||||
@@ -1928,6 +1929,30 @@ export type OpenClawPluginCliCommandDescriptor = {
|
||||
hasSubcommands: boolean;
|
||||
};
|
||||
|
||||
export type OpenClawPluginReloadRegistration = {
|
||||
restartPrefixes?: string[];
|
||||
hotPrefixes?: string[];
|
||||
noopPrefixes?: string[];
|
||||
};
|
||||
|
||||
export type OpenClawPluginNodeHostCommand = {
|
||||
command: string;
|
||||
cap?: string;
|
||||
handle: (paramsJSON?: string | null) => Promise<string>;
|
||||
};
|
||||
|
||||
export type OpenClawPluginSecurityAuditContext = {
|
||||
config: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
};
|
||||
|
||||
export type OpenClawPluginSecurityAuditCollector = (
|
||||
ctx: OpenClawPluginSecurityAuditContext,
|
||||
) => SecurityAuditFinding[] | Promise<SecurityAuditFinding[]>;
|
||||
|
||||
/** Context passed to long-lived plugin services. */
|
||||
export type OpenClawPluginServiceContext = {
|
||||
config: OpenClawConfig;
|
||||
@@ -1955,6 +1980,9 @@ export type OpenClawPluginDefinition = {
|
||||
version?: string;
|
||||
kind?: PluginKind | PluginKind[];
|
||||
configSchema?: OpenClawPluginConfigSchema;
|
||||
reload?: OpenClawPluginReloadRegistration;
|
||||
nodeHostCommands?: OpenClawPluginNodeHostCommand[];
|
||||
securityAuditCollectors?: OpenClawPluginSecurityAuditCollector[];
|
||||
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
||||
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
|
||||
};
|
||||
@@ -2040,6 +2068,9 @@ export type OpenClawPluginApi = {
|
||||
descriptors?: OpenClawPluginCliCommandDescriptor[];
|
||||
},
|
||||
) => void;
|
||||
registerReload: (registration: OpenClawPluginReloadRegistration) => void;
|
||||
registerNodeHostCommand: (command: OpenClawPluginNodeHostCommand) => void;
|
||||
registerSecurityAuditCollector: (collector: OpenClawPluginSecurityAuditCollector) => void;
|
||||
registerService: (service: OpenClawPluginService) => void;
|
||||
/** Register a lightweight config migration that can run before plugin runtime loads. */
|
||||
registerConfigMigration: (migrate: PluginConfigMigration) => void;
|
||||
|
||||
Reference in New Issue
Block a user