mirror of https://github.com/openclaw/openclaw.git
perf: reduce plugin runtime startup overhead
This commit is contained in:
parent
bb16ab9e08
commit
3fa2300ba1
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export {
|
|||
PAIRING_APPROVED_MESSAGE,
|
||||
projectCredentialSnapshotFields,
|
||||
resolveConfiguredFromCredentialStatuses,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
export {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
} from "../agents/tools/common.js";
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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.`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue