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

@ -1,15 +1,28 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import {
listInspectedDirectoryEntriesFromSources,
listResolvedDirectoryEntriesFromSources,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import { inspectDiscordAccount, type InspectedDiscordAccount } from "./account-inspect.js";
import { mergeDiscordAccountConfig, resolveDefaultDiscordAccountId } from "./accounts.js";
function resolveDiscordDirectoryConfigAccount(
cfg: DirectoryConfigParams["cfg"],
accountId?: string | null,
) {
const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultDiscordAccountId(cfg));
const config = mergeDiscordAccountConfig(cfg, resolvedAccountId);
return {
accountId: resolvedAccountId,
config,
dm: config.dm,
};
}
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
return listInspectedDirectoryEntriesFromSources({
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "user",
inspectAccount: (cfg, accountId) =>
inspectDiscordAccount({ cfg, accountId }) as InspectedDiscordAccount | null,
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => {
const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? [];
const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [
@ -27,11 +40,10 @@ export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfi
}
export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
return listInspectedDirectoryEntriesFromSources({
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "group",
inspectAccount: (cfg, accountId) =>
inspectDiscordAccount({ cfg, accountId }) as InspectedDiscordAccount | null,
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) =>
Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})),
normalizeId: (raw) => {

View File

@ -4,7 +4,7 @@ export {
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
} from "openclaw/plugin-sdk/discord";
} from "openclaw/plugin-sdk/channel-status";
export {
buildChannelConfigSchema,
getChatChannelMeta,

View File

@ -5,10 +5,10 @@ import {
} from "openclaw/plugin-sdk/channel-contract";
import type { SlackActionContext } from "./action-runtime.js";
import { handleSlackAction } from "./action-runtime.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
import { handleSlackMessageAction } from "./message-action-dispatch.js";
import { extractSlackToolSend, listSlackMessageActions } from "./message-actions.js";
import { createSlackMessageToolBlocksSchema } from "./message-tool-schema.js";
import { isSlackInteractiveRepliesEnabled } from "./runtime-api.js";
import { resolveSlackChannelId } from "./targets.js";
type SlackActionInvoke = (

View File

@ -1,16 +1,29 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-resolution";
import {
listInspectedDirectoryEntriesFromSources,
listResolvedDirectoryEntriesFromSources,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import { inspectSlackAccount, type InspectedSlackAccount } from "./account-inspect.js";
import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js";
import { parseSlackTarget } from "./targets.js";
function resolveSlackDirectoryConfigAccount(
cfg: DirectoryConfigParams["cfg"],
accountId?: string | null,
) {
const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultSlackAccountId(cfg));
const config = mergeSlackAccountConfig(cfg, resolvedAccountId);
return {
accountId: resolvedAccountId,
config,
dm: config.dm,
};
}
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
return listInspectedDirectoryEntriesFromSources({
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "user",
inspectAccount: (cfg, accountId) =>
inspectSlackAccount({ cfg, accountId }) as InspectedSlackAccount | null,
resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => {
const allowFrom = account.config.allowFrom ?? account.dm?.allowFrom ?? [];
const channelUsers = Object.values(account.config.channels ?? {}).flatMap(
@ -32,11 +45,10 @@ export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigP
}
export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
return listInspectedDirectoryEntriesFromSources({
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "group",
inspectAccount: (cfg, accountId) =>
inspectSlackAccount({ cfg, accountId }) as InspectedSlackAccount | null,
resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => [Object.keys(account.config.channels ?? {})],
normalizeId: (raw) => {
const normalized = parseSlackTarget(raw, { defaultKind: "channel" });

View File

@ -1,3 +1,4 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-resolution";
import type { ChannelGroupContext } from "openclaw/plugin-sdk/channel-contract";
import {
resolveToolsBySender,
@ -5,7 +6,7 @@ import {
type GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/channel-policy";
import { normalizeHyphenSlug } from "openclaw/plugin-sdk/core";
import { inspectSlackAccount } from "./account-inspect.js";
import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js";
type SlackChannelPolicyEntry = {
requireMention?: boolean;
@ -16,12 +17,14 @@ type SlackChannelPolicyEntry = {
function resolveSlackChannelPolicyEntry(
params: ChannelGroupContext,
): SlackChannelPolicyEntry | undefined {
const account = inspectSlackAccount({
cfg: params.cfg,
accountId: params.accountId,
});
const channels = (account.channels ?? {}) as Record<string, SlackChannelPolicyEntry>;
if (Object.keys(channels).length === 0) {
const accountId = normalizeAccountId(
params.accountId ?? resolveDefaultSlackAccountId(params.cfg),
);
const channels = mergeSlackAccountConfig(params.cfg, accountId).channels as
| Record<string, SlackChannelPolicyEntry>
| undefined;
const channelMap = channels ?? {};
if (Object.keys(channelMap).length === 0) {
return undefined;
}
const channelId = params.groupId?.trim();
@ -35,11 +38,11 @@ function resolveSlackChannelPolicyEntry(
normalizedName,
].filter(Boolean);
for (const candidate of candidates) {
if (candidate && channels[candidate]) {
return channels[candidate];
if (candidate && channelMap[candidate]) {
return channelMap[candidate];
}
}
return channels["*"];
return channelMap["*"];
}
function resolveSenderToolsEntry(

View File

@ -1,9 +1,9 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-contract";
import { normalizeInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime";
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
import { parseSlackBlocksInput } from "./blocks-input.js";
import { buildSlackInteractiveBlocks } from "./blocks-render.js";
import { readNumberParam, readStringParam } from "./runtime-api.js";
type SlackActionInvoke = (
action: Record<string, unknown>,

View File

@ -1,19 +1,15 @@
export {
buildComputedAccountStatusSnapshot,
DEFAULT_ACCOUNT_ID,
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
type ChannelPlugin,
type OpenClawConfig,
type SlackAccountConfig,
} from "openclaw/plugin-sdk/slack";
} from "openclaw/plugin-sdk/channel-status";
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
export {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "./directory-config.js";
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
} from "openclaw/plugin-sdk/slack-targets";
export type { ChannelPlugin, OpenClawConfig, SlackAccountConfig } from "openclaw/plugin-sdk/slack";
export {
buildChannelConfigSchema,
getChatChannelMeta,
@ -26,4 +22,3 @@ export {
SlackConfigSchema,
withNormalizedTimestamp,
} from "openclaw/plugin-sdk/slack-core";
export { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";

View File

@ -449,6 +449,10 @@
"types": "./dist/plugin-sdk/provider-web-search.d.ts",
"default": "./dist/plugin-sdk/provider-web-search.js"
},
"./plugin-sdk/param-readers": {
"types": "./dist/plugin-sdk/param-readers.d.ts",
"default": "./dist/plugin-sdk/param-readers.js"
},
"./plugin-sdk/provider-zai-endpoint": {
"types": "./dist/plugin-sdk/provider-zai-endpoint.d.ts",
"default": "./dist/plugin-sdk/provider-zai-endpoint.js"
@ -461,6 +465,10 @@
"types": "./dist/plugin-sdk/signal.d.ts",
"default": "./dist/plugin-sdk/signal.js"
},
"./plugin-sdk/channel-status": {
"types": "./dist/plugin-sdk/channel-status.d.ts",
"default": "./dist/plugin-sdk/channel-status.js"
},
"./plugin-sdk/slack": {
"types": "./dist/plugin-sdk/slack.d.ts",
"default": "./dist/plugin-sdk/slack.js"
@ -469,6 +477,10 @@
"types": "./dist/plugin-sdk/slack-core.d.ts",
"default": "./dist/plugin-sdk/slack-core.js"
},
"./plugin-sdk/slack-targets": {
"types": "./dist/plugin-sdk/slack-targets.d.ts",
"default": "./dist/plugin-sdk/slack-targets.js"
},
"./plugin-sdk/status-helpers": {
"types": "./dist/plugin-sdk/status-helpers.d.ts",
"default": "./dist/plugin-sdk/status-helpers.js"

View File

@ -102,11 +102,14 @@
"provider-stream",
"provider-usage",
"provider-web-search",
"param-readers",
"provider-zai-endpoint",
"secret-input",
"signal",
"channel-status",
"slack",
"slack-core",
"slack-targets",
"status-helpers",
"speech",
"state-paths",

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,