perf: reduce plugin runtime startup overhead

This commit is contained in:
Peter Steinberger
2026-03-22 16:32:17 +00:00
parent bb16ab9e08
commit 3fa2300ba1
18 changed files with 309 additions and 172 deletions

View File

@@ -0,0 +1,10 @@
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export {
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
resolveConfiguredFromRequiredCredentialStatuses,
} from "../channels/account-snapshot-fields.js";
export {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
} from "./status-helpers.js";

View File

@@ -0,0 +1,6 @@
export {
readNumberParam,
readStringArrayParam,
readStringOrNumberParam,
readStringParam,
} from "../agents/tools/common.js";

View File

@@ -1,6 +1,5 @@
export {
parseSlackTarget,
resolveSlackChannelId,
type SlackTarget,
type SlackTargetKind,
} from "../../extensions/slack/api.js";
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
} from "../channels/plugins/normalize/slack.js";
export { parseSlackTarget, resolveSlackChannelId } from "../../extensions/slack/src/targets.js";

View File

@@ -0,0 +1,74 @@
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import type { OpenClawPluginCommandDefinition } from "./types.js";
export type RegisteredPluginCommand = OpenClawPluginCommandDefinition & {
pluginId: string;
pluginName?: string;
pluginRoot?: string;
};
type PluginCommandState = {
pluginCommands: Map<string, RegisteredPluginCommand>;
registryLocked: boolean;
};
const PLUGIN_COMMAND_STATE_KEY = Symbol.for("openclaw.pluginCommandsState");
const state = resolveGlobalSingleton<PluginCommandState>(PLUGIN_COMMAND_STATE_KEY, () => ({
pluginCommands: new Map<string, RegisteredPluginCommand>(),
registryLocked: false,
}));
export const pluginCommands = state.pluginCommands;
export function isPluginCommandRegistryLocked(): boolean {
return state.registryLocked;
}
export function setPluginCommandRegistryLocked(locked: boolean): void {
state.registryLocked = locked;
}
export function clearPluginCommands(): void {
pluginCommands.clear();
}
export function clearPluginCommandsForPlugin(pluginId: string): void {
for (const [key, cmd] of pluginCommands.entries()) {
if (cmd.pluginId === pluginId) {
pluginCommands.delete(key);
}
}
}
function resolvePluginNativeName(
command: OpenClawPluginCommandDefinition,
provider?: string,
): string {
const providerName = provider?.trim().toLowerCase();
const providerOverride = providerName ? command.nativeNames?.[providerName] : undefined;
if (typeof providerOverride === "string" && providerOverride.trim()) {
return providerOverride.trim();
}
const defaultOverride = command.nativeNames?.default;
if (typeof defaultOverride === "string" && defaultOverride.trim()) {
return defaultOverride.trim();
}
return command.name;
}
export function getPluginCommandSpecs(provider?: string): Array<{
name: string;
description: string;
acceptsArgs: boolean;
}> {
const providerName = provider?.trim().toLowerCase();
if (providerName && providerName !== "telegram" && providerName !== "discord") {
return [];
}
return Array.from(pluginCommands.values()).map((cmd) => ({
name: resolvePluginNativeName(cmd, provider),
description: cmd.description,
acceptsArgs: cmd.acceptsArgs ?? false,
}));
}

View File

@@ -8,7 +8,15 @@
import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js";
import type { OpenClawConfig } from "../config/config.js";
import { logVerbose } from "../globals.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import {
clearPluginCommands,
clearPluginCommandsForPlugin,
getPluginCommandSpecs,
isPluginCommandRegistryLocked,
pluginCommands,
setPluginCommandRegistryLocked,
type RegisteredPluginCommand,
} from "./command-registry-state.js";
import {
detachPluginConversationBinding,
getCurrentPluginConversationBinding,
@@ -20,26 +28,6 @@ import type {
PluginCommandResult,
} from "./types.js";
type RegisteredPluginCommand = OpenClawPluginCommandDefinition & {
pluginId: string;
pluginName?: string;
pluginRoot?: string;
};
type PluginCommandState = {
pluginCommands: Map<string, RegisteredPluginCommand>;
registryLocked: boolean;
};
const PLUGIN_COMMAND_STATE_KEY = Symbol.for("openclaw.pluginCommandsState");
const state = resolveGlobalSingleton<PluginCommandState>(PLUGIN_COMMAND_STATE_KEY, () => ({
pluginCommands: new Map<string, RegisteredPluginCommand>(),
registryLocked: false,
}));
const pluginCommands = state.pluginCommands;
// Maximum allowed length for command arguments (defense in depth)
const MAX_ARGS_LENGTH = 4096;
@@ -181,7 +169,7 @@ export function registerPluginCommand(
opts?: { pluginName?: string; pluginRoot?: string },
): CommandRegistrationResult {
// Prevent registration while commands are being processed
if (state.registryLocked) {
if (isPluginCommandRegistryLocked()) {
return { ok: false, error: "Cannot register commands while processing is in progress" };
}
@@ -225,24 +213,7 @@ export function registerPluginCommand(
return { ok: true };
}
/**
* Clear all registered plugin commands.
* Called during plugin reload.
*/
export function clearPluginCommands(): void {
pluginCommands.clear();
}
/**
* Clear plugin commands for a specific plugin.
*/
export function clearPluginCommandsForPlugin(pluginId: string): void {
for (const [key, cmd] of pluginCommands.entries()) {
if (cmd.pluginId === pluginId) {
pluginCommands.delete(key);
}
}
}
export { clearPluginCommands, clearPluginCommandsForPlugin, getPluginCommandSpecs };
/**
* Check if a command body matches a registered plugin command.
@@ -460,7 +431,7 @@ export async function executePluginCommand(params: {
};
// Lock registry during execution to prevent concurrent modifications
state.registryLocked = true;
setPluginCommandRegistryLocked(true);
try {
const result = await command.handler(ctx);
logVerbose(
@@ -473,7 +444,7 @@ export async function executePluginCommand(params: {
// Don't leak internal error details - return a safe generic message
return { text: "⚠️ Command failed. Please try again later." };
} finally {
state.registryLocked = false;
setPluginCommandRegistryLocked(false);
}
}
@@ -493,45 +464,10 @@ export function listPluginCommands(): Array<{
}));
}
function resolvePluginNativeName(
command: OpenClawPluginCommandDefinition,
provider?: string,
): string {
const providerName = provider?.trim().toLowerCase();
const providerOverride = providerName ? command.nativeNames?.[providerName] : undefined;
if (typeof providerOverride === "string" && providerOverride.trim()) {
return providerOverride.trim();
}
const defaultOverride = command.nativeNames?.default;
if (typeof defaultOverride === "string" && defaultOverride.trim()) {
return defaultOverride.trim();
}
return command.name;
}
function listPluginInvocationNames(command: OpenClawPluginCommandDefinition): string[] {
return listPluginInvocationKeys(command);
}
/**
* Get plugin command specs for native command registration (e.g., Telegram).
*/
export function getPluginCommandSpecs(provider?: string): Array<{
name: string;
description: string;
acceptsArgs: boolean;
}> {
const providerName = provider?.trim().toLowerCase();
if (providerName && providerName !== "telegram" && providerName !== "discord") {
return [];
}
return Array.from(pluginCommands.values()).map((cmd) => ({
name: resolvePluginNativeName(cmd, provider),
description: cmd.description,
acceptsArgs: cmd.acceptsArgs ?? false,
}));
}
export const __testing = {
resolveBindingConversationFromCommand,
};

View File

@@ -0,0 +1,86 @@
import path from "node:path";
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
type InstallScanLogger = {
warn?: (message: string) => void;
};
function buildCriticalDetails(params: {
findings: Array<{ file: string; line: number; message: string; severity: string }>;
}) {
return params.findings
.filter((finding) => finding.severity === "critical")
.map((finding) => `${finding.message} (${finding.file}:${finding.line})`)
.join("; ");
}
export async function scanBundleInstallSourceRuntime(params: {
logger: InstallScanLogger;
pluginId: string;
sourceDir: string;
}) {
try {
const scanSummary = await scanDirectoryWithSummary(params.sourceDir);
if (scanSummary.critical > 0) {
params.logger.warn?.(
`WARNING: Bundle "${params.pluginId}" contains dangerous code patterns: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
);
return;
}
if (scanSummary.warn > 0) {
params.logger.warn?.(
`Bundle "${params.pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
);
}
} catch (err) {
params.logger.warn?.(
`Bundle "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
}
}
export async function scanPackageInstallSourceRuntime(params: {
extensions: string[];
logger: InstallScanLogger;
packageDir: string;
pluginId: string;
}) {
const forcedScanEntries: string[] = [];
for (const entry of params.extensions) {
const resolvedEntry = path.resolve(params.packageDir, entry);
if (!isPathInside(params.packageDir, resolvedEntry)) {
params.logger.warn?.(
`extension entry escapes plugin directory and will not be scanned: ${entry}`,
);
continue;
}
if (extensionUsesSkippedScannerPath(entry)) {
params.logger.warn?.(
`extension entry is in a hidden/node_modules path and will receive targeted scan coverage: ${entry}`,
);
}
forcedScanEntries.push(resolvedEntry);
}
try {
const scanSummary = await scanDirectoryWithSummary(params.packageDir, {
includeFiles: forcedScanEntries,
});
if (scanSummary.critical > 0) {
params.logger.warn?.(
`WARNING: Plugin "${params.pluginId}" contains dangerous code patterns: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
);
return;
}
if (scanSummary.warn > 0) {
params.logger.warn?.(
`Plugin "${params.pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
);
}
} catch (err) {
params.logger.warn?.(
`Plugin "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
}
}

View File

@@ -0,0 +1,26 @@
type InstallScanLogger = {
warn?: (message: string) => void;
};
async function loadInstallSecurityScanRuntime() {
return await import("./install-security-scan.runtime.js");
}
export async function scanBundleInstallSource(params: {
logger: InstallScanLogger;
pluginId: string;
sourceDir: string;
}) {
const { scanBundleInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
await scanBundleInstallSourceRuntime(params);
}
export async function scanPackageInstallSource(params: {
extensions: string[];
logger: InstallScanLogger;
packageDir: string;
pluginId: string;
}) {
const { scanPackageInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
await scanPackageInstallSourceRuntime(params);
}

View File

@@ -28,11 +28,11 @@ import {
installFromNpmSpecArchiveWithInstaller,
} from "../infra/npm-pack-install.js";
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
import * as skillScanner from "../security/skill-scanner.js";
import { isPathInside } from "../security/scan-paths.js";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
import { resolveRuntimeServiceVersion } from "../version.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
import {
getPackageManifestMetadata,
loadPluginManifest,
@@ -385,20 +385,11 @@ async function installBundleFromSourceDir(
}
try {
const scanSummary = await skillScanner.scanDirectoryWithSummary(params.sourceDir);
if (scanSummary.critical > 0) {
const criticalDetails = scanSummary.findings
.filter((f) => f.severity === "critical")
.map((f) => `${f.message} (${f.file}:${f.line})`)
.join("; ");
logger.warn?.(
`WARNING: Bundle "${pluginId}" contains dangerous code patterns: ${criticalDetails}`,
);
} else if (scanSummary.warn > 0) {
logger.warn?.(
`Bundle "${pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
);
}
await scanBundleInstallSource({
sourceDir: params.sourceDir,
pluginId,
logger,
});
} catch (err) {
logger.warn?.(
`Bundle "${pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
@@ -557,41 +548,13 @@ async function installPluginFromPackageDir(
code: PLUGIN_INSTALL_ERROR_CODE.INCOMPATIBLE_HOST_VERSION,
};
}
const packageDir = path.resolve(params.packageDir);
const forcedScanEntries: string[] = [];
for (const entry of extensions) {
const resolvedEntry = path.resolve(packageDir, entry);
if (!isPathInside(packageDir, resolvedEntry)) {
logger.warn?.(`extension entry escapes plugin directory and will not be scanned: ${entry}`);
continue;
}
if (extensionUsesSkippedScannerPath(entry)) {
logger.warn?.(
`extension entry is in a hidden/node_modules path and will receive targeted scan coverage: ${entry}`,
);
}
forcedScanEntries.push(resolvedEntry);
}
// Scan plugin source for dangerous code patterns (warn-only; never blocks install)
try {
const scanSummary = await skillScanner.scanDirectoryWithSummary(params.packageDir, {
includeFiles: forcedScanEntries,
await scanPackageInstallSource({
packageDir: params.packageDir,
pluginId,
logger,
extensions,
});
if (scanSummary.critical > 0) {
const criticalDetails = scanSummary.findings
.filter((f) => f.severity === "critical")
.map((f) => `${f.message} (${f.file}:${f.line})`)
.join("; ");
logger.warn?.(
`WARNING: Plugin "${pluginId}" contains dangerous code patterns: ${criticalDetails}`,
);
} else if (scanSummary.warn > 0) {
logger.warn?.(
`Plugin "${pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
);
}
} catch (err) {
logger.warn?.(
`Plugin "${pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,

View File

@@ -15,7 +15,7 @@ import {
} from "../memory/prompt-section.js";
import { resolveUserPath } from "../utils.js";
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
import { clearPluginCommands } from "./commands.js";
import { clearPluginCommands } from "./command-registry-state.js";
import {
applyTestPluginDefaults,
normalizePluginsConfig,