mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
plugins: add before_install hook for install scanners
This commit is contained in:
@@ -56,6 +56,9 @@ import type {
|
||||
PluginHookToolResultPersistResult,
|
||||
PluginHookBeforeMessageWriteEvent,
|
||||
PluginHookBeforeMessageWriteResult,
|
||||
PluginHookBeforeInstallContext,
|
||||
PluginHookBeforeInstallEvent,
|
||||
PluginHookBeforeInstallResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Re-export types for consumers
|
||||
@@ -106,6 +109,9 @@ export type {
|
||||
PluginHookGatewayContext,
|
||||
PluginHookGatewayStartEvent,
|
||||
PluginHookGatewayStopEvent,
|
||||
PluginHookBeforeInstallContext,
|
||||
PluginHookBeforeInstallEvent,
|
||||
PluginHookBeforeInstallResult,
|
||||
};
|
||||
|
||||
export type HookRunnerLogger = {
|
||||
@@ -977,6 +983,41 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
||||
return runVoidHook("gateway_stop", event, ctx);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Skill Install Hooks
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Run before_install hook.
|
||||
* Allows plugins to augment scan findings or block installs.
|
||||
* Runs sequentially so higher-priority hooks can block before lower ones run.
|
||||
*/
|
||||
async function runBeforeInstall(
|
||||
event: PluginHookBeforeInstallEvent,
|
||||
ctx: PluginHookBeforeInstallContext,
|
||||
): Promise<PluginHookBeforeInstallResult | undefined> {
|
||||
return runModifyingHook<"before_install", PluginHookBeforeInstallResult>(
|
||||
"before_install",
|
||||
event,
|
||||
ctx,
|
||||
{
|
||||
mergeResults: (acc, next) => {
|
||||
if (acc?.block === true) {
|
||||
return acc;
|
||||
}
|
||||
const mergedFindings = [...(acc?.findings ?? []), ...(next.findings ?? [])];
|
||||
return {
|
||||
findings: mergedFindings.length > 0 ? mergedFindings : undefined,
|
||||
block: stickyTrue(acc?.block, next.block),
|
||||
blockReason: lastDefined(acc?.blockReason, next.blockReason),
|
||||
};
|
||||
},
|
||||
shouldStop: (result) => result.block === true,
|
||||
terminalLabel: "block=true",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Utility
|
||||
// =========================================================================
|
||||
@@ -1030,6 +1071,8 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
||||
// Gateway hooks
|
||||
runGatewayStart,
|
||||
runGatewayStop,
|
||||
// Install hooks
|
||||
runBeforeInstall,
|
||||
// Utility
|
||||
hasHooks,
|
||||
getHookCount,
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import path from "node:path";
|
||||
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
|
||||
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
|
||||
import { getGlobalHookRunner } from "./hook-runner-global.js";
|
||||
|
||||
type InstallScanLogger = {
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
type InstallScanFinding = {
|
||||
ruleId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
file: string;
|
||||
line: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type InstallSecurityScanResult = {
|
||||
blocked?: {
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
function buildCriticalDetails(params: {
|
||||
findings: Array<{ file: string; line: number; message: string; severity: string }>;
|
||||
}) {
|
||||
@@ -15,20 +30,66 @@ function buildCriticalDetails(params: {
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
async function runBeforeInstallHook(params: {
|
||||
logger: InstallScanLogger;
|
||||
installLabel: string;
|
||||
source: string;
|
||||
sourceDir: string;
|
||||
targetName: string;
|
||||
targetType: "skill" | "plugin";
|
||||
builtinFindings: InstallScanFinding[];
|
||||
}): Promise<InstallSecurityScanResult | undefined> {
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
if (!hookRunner?.hasHooks("before_install")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const hookResult = await hookRunner.runBeforeInstall(
|
||||
{
|
||||
targetName: params.targetName,
|
||||
targetType: params.targetType,
|
||||
source: params.source,
|
||||
sourceDir: params.sourceDir,
|
||||
builtinFindings: params.builtinFindings,
|
||||
},
|
||||
{ source: params.source, targetType: params.targetType },
|
||||
);
|
||||
if (hookResult?.block) {
|
||||
const reason = hookResult.blockReason || "Installation blocked by plugin hook";
|
||||
params.logger.warn?.(`WARNING: ${params.installLabel} blocked by plugin hook: ${reason}`);
|
||||
return { blocked: { reason } };
|
||||
}
|
||||
if (hookResult?.findings) {
|
||||
for (const finding of hookResult.findings) {
|
||||
if (finding.severity === "critical" || finding.severity === "warn") {
|
||||
params.logger.warn?.(
|
||||
`Plugin scanner: ${finding.message} (${finding.file}:${finding.line})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Hook errors are non-fatal.
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function scanBundleInstallSourceRuntime(params: {
|
||||
logger: InstallScanLogger;
|
||||
pluginId: string;
|
||||
sourceDir: string;
|
||||
}) {
|
||||
}): Promise<InstallSecurityScanResult | undefined> {
|
||||
let builtinFindings: InstallScanFinding[] = [];
|
||||
try {
|
||||
const scanSummary = await scanDirectoryWithSummary(params.sourceDir);
|
||||
builtinFindings = scanSummary.findings;
|
||||
if (scanSummary.critical > 0) {
|
||||
params.logger.warn?.(
|
||||
`WARNING: Bundle "${params.pluginId}" contains dangerous code patterns: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (scanSummary.warn > 0) {
|
||||
} else if (scanSummary.warn > 0) {
|
||||
params.logger.warn?.(
|
||||
`Bundle "${params.pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
|
||||
);
|
||||
@@ -38,6 +99,16 @@ export async function scanBundleInstallSourceRuntime(params: {
|
||||
`Bundle "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
|
||||
);
|
||||
}
|
||||
|
||||
return await runBeforeInstallHook({
|
||||
logger: params.logger,
|
||||
installLabel: `Bundle "${params.pluginId}" installation`,
|
||||
source: "plugin-bundle",
|
||||
sourceDir: params.sourceDir,
|
||||
targetName: params.pluginId,
|
||||
targetType: "plugin",
|
||||
builtinFindings,
|
||||
});
|
||||
}
|
||||
|
||||
export async function scanPackageInstallSourceRuntime(params: {
|
||||
@@ -45,7 +116,7 @@ export async function scanPackageInstallSourceRuntime(params: {
|
||||
logger: InstallScanLogger;
|
||||
packageDir: string;
|
||||
pluginId: string;
|
||||
}) {
|
||||
}): Promise<InstallSecurityScanResult | undefined> {
|
||||
const forcedScanEntries: string[] = [];
|
||||
for (const entry of params.extensions) {
|
||||
const resolvedEntry = path.resolve(params.packageDir, entry);
|
||||
@@ -63,17 +134,17 @@ export async function scanPackageInstallSourceRuntime(params: {
|
||||
forcedScanEntries.push(resolvedEntry);
|
||||
}
|
||||
|
||||
let builtinFindings: InstallScanFinding[] = [];
|
||||
try {
|
||||
const scanSummary = await scanDirectoryWithSummary(params.packageDir, {
|
||||
includeFiles: forcedScanEntries,
|
||||
});
|
||||
builtinFindings = scanSummary.findings;
|
||||
if (scanSummary.critical > 0) {
|
||||
params.logger.warn?.(
|
||||
`WARNING: Plugin "${params.pluginId}" contains dangerous code patterns: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (scanSummary.warn > 0) {
|
||||
} else if (scanSummary.warn > 0) {
|
||||
params.logger.warn?.(
|
||||
`Plugin "${params.pluginId}" has ${scanSummary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
|
||||
);
|
||||
@@ -83,4 +154,14 @@ export async function scanPackageInstallSourceRuntime(params: {
|
||||
`Plugin "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
|
||||
);
|
||||
}
|
||||
|
||||
return await runBeforeInstallHook({
|
||||
logger: params.logger,
|
||||
installLabel: `Plugin "${params.pluginId}" installation`,
|
||||
source: "plugin-package",
|
||||
sourceDir: params.packageDir,
|
||||
targetName: params.pluginId,
|
||||
targetType: "plugin",
|
||||
builtinFindings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ type InstallScanLogger = {
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type InstallSecurityScanResult = {
|
||||
blocked?: {
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function loadInstallSecurityScanRuntime() {
|
||||
return await import("./install-security-scan.runtime.js");
|
||||
}
|
||||
@@ -10,9 +16,9 @@ export async function scanBundleInstallSource(params: {
|
||||
logger: InstallScanLogger;
|
||||
pluginId: string;
|
||||
sourceDir: string;
|
||||
}) {
|
||||
}): Promise<InstallSecurityScanResult | undefined> {
|
||||
const { scanBundleInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
|
||||
await scanBundleInstallSourceRuntime(params);
|
||||
return await scanBundleInstallSourceRuntime(params);
|
||||
}
|
||||
|
||||
export async function scanPackageInstallSource(params: {
|
||||
@@ -20,7 +26,7 @@ export async function scanPackageInstallSource(params: {
|
||||
logger: InstallScanLogger;
|
||||
packageDir: string;
|
||||
pluginId: string;
|
||||
}) {
|
||||
}): Promise<InstallSecurityScanResult | undefined> {
|
||||
const { scanPackageInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
|
||||
await scanPackageInstallSourceRuntime(params);
|
||||
return await scanPackageInstallSourceRuntime(params);
|
||||
}
|
||||
|
||||
@@ -376,11 +376,14 @@ async function installBundleFromSourceDir(
|
||||
}
|
||||
|
||||
try {
|
||||
await runtime.scanBundleInstallSource({
|
||||
const scanResult = await runtime.scanBundleInstallSource({
|
||||
sourceDir: params.sourceDir,
|
||||
pluginId,
|
||||
logger,
|
||||
});
|
||||
if (scanResult?.blocked) {
|
||||
return { ok: false, error: scanResult.blocked.reason };
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
`Bundle "${pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
|
||||
@@ -545,12 +548,15 @@ async function installPluginFromPackageDir(
|
||||
};
|
||||
}
|
||||
try {
|
||||
await runtime.scanPackageInstallSource({
|
||||
const scanResult = await runtime.scanPackageInstallSource({
|
||||
packageDir: params.packageDir,
|
||||
pluginId,
|
||||
logger,
|
||||
extensions,
|
||||
});
|
||||
if (scanResult?.blocked) {
|
||||
return { ok: false, error: scanResult.blocked.reason };
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
`Plugin "${pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
|
||||
|
||||
@@ -1803,7 +1803,8 @@ export type PluginHookName =
|
||||
| "subagent_ended"
|
||||
| "gateway_start"
|
||||
| "gateway_stop"
|
||||
| "before_dispatch";
|
||||
| "before_dispatch"
|
||||
| "before_install";
|
||||
|
||||
export const PLUGIN_HOOK_NAMES = [
|
||||
"before_model_resolve",
|
||||
@@ -1832,6 +1833,7 @@ export const PLUGIN_HOOK_NAMES = [
|
||||
"gateway_start",
|
||||
"gateway_stop",
|
||||
"before_dispatch",
|
||||
"before_install",
|
||||
] as const satisfies readonly PluginHookName[];
|
||||
|
||||
type MissingPluginHookNames = Exclude<PluginHookName, (typeof PLUGIN_HOOK_NAMES)[number]>;
|
||||
@@ -2337,6 +2339,50 @@ export type PluginHookGatewayStopEvent = {
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type PluginInstallTargetType = "skill" | "plugin";
|
||||
|
||||
// before_install hook
|
||||
export type PluginHookBeforeInstallContext = {
|
||||
/** Category of install target being checked. */
|
||||
targetType: PluginInstallTargetType;
|
||||
/** Origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type PluginHookBeforeInstallEvent = {
|
||||
/** Category of install target being checked. */
|
||||
targetType: PluginInstallTargetType;
|
||||
/** Human-readable skill or plugin name. */
|
||||
targetName: string;
|
||||
/** Absolute path to the install target source directory being scanned. */
|
||||
sourceDir: string;
|
||||
/** Origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
|
||||
source?: string;
|
||||
/** Findings from the built-in scanner, provided for augmentation. */
|
||||
builtinFindings: Array<{
|
||||
ruleId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
file: string;
|
||||
line: number;
|
||||
message: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type PluginHookBeforeInstallResult = {
|
||||
/** Additional findings to merge with built-in scanner results. */
|
||||
findings?: Array<{
|
||||
ruleId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
file: string;
|
||||
line: number;
|
||||
message: string;
|
||||
}>;
|
||||
/** If true, block the installation entirely. */
|
||||
block?: boolean;
|
||||
/** Human-readable reason for blocking. */
|
||||
blockReason?: string;
|
||||
};
|
||||
|
||||
// Hook handler types mapped by hook name
|
||||
export type PluginHookHandlerMap = {
|
||||
before_model_resolve: (
|
||||
@@ -2443,6 +2489,10 @@ export type PluginHookHandlerMap = {
|
||||
event: PluginHookGatewayStopEvent,
|
||||
ctx: PluginHookGatewayContext,
|
||||
) => Promise<void> | void;
|
||||
before_install: (
|
||||
event: PluginHookBeforeInstallEvent,
|
||||
ctx: PluginHookBeforeInstallContext,
|
||||
) => Promise<PluginHookBeforeInstallResult | void> | PluginHookBeforeInstallResult | void;
|
||||
};
|
||||
|
||||
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
|
||||
|
||||
Reference in New Issue
Block a user