plugins: enrich before_install policy context

This commit is contained in:
George Zhang 2026-03-29 10:50:12 -07:00
parent 9ea0b76f06
commit 150faba8d1
6 changed files with 495 additions and 82 deletions

View File

@ -56,26 +56,44 @@ function formatScanFindingDetail(
return `${finding.message} (${filePath}:${finding.line})`;
}
type SkillScanFinding = {
ruleId: string;
severity: "info" | "warn" | "critical";
file: string;
line: number;
message: string;
};
type SkillBuiltinScan = {
status: "ok" | "error";
scannedFiles: number;
critical: number;
warn: number;
info: number;
findings: SkillScanFinding[];
error?: string;
};
type SkillScanResult = {
warnings: string[];
findings: Array<{
ruleId: string;
severity: "info" | "warn" | "critical";
file: string;
line: number;
message: string;
}>;
builtinScan: SkillBuiltinScan;
};
async function collectSkillInstallScanWarnings(entry: SkillEntry): Promise<SkillScanResult> {
const warnings: string[] = [];
const findings: SkillScanResult["findings"] = [];
const skillName = entry.skill.name;
const skillDir = path.resolve(entry.skill.baseDir);
try {
const summary = await scanDirectoryWithSummary(skillDir);
findings.push(...summary.findings);
const builtinScan: SkillBuiltinScan = {
status: "ok",
scannedFiles: summary.scannedFiles,
critical: summary.critical,
warn: summary.warn,
info: summary.info,
findings: summary.findings,
};
if (summary.critical > 0) {
const criticalDetails = summary.findings
.filter((finding) => finding.severity === "critical")
@ -89,13 +107,24 @@ async function collectSkillInstallScanWarnings(entry: SkillEntry): Promise<Skill
`Skill "${skillName}" has ${summary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
);
}
return { warnings, builtinScan };
} catch (err) {
warnings.push(
`Skill "${skillName}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
return {
warnings,
builtinScan: {
status: "error",
scannedFiles: 0,
critical: 0,
warn: 0,
info: 0,
findings: [],
error: String(err),
},
};
}
return { warnings, findings };
}
function resolveInstallId(spec: SkillInstallSpec, index: number): string {
@ -112,6 +141,24 @@ function findInstallSpec(entry: SkillEntry, installId: string): SkillInstallSpec
return undefined;
}
function normalizeSkillInstallSpec(spec: SkillInstallSpec): SkillInstallSpec {
return {
...(spec.id ? { id: spec.id } : {}),
kind: spec.kind,
...(spec.label ? { label: spec.label } : {}),
...(spec.bins ? { bins: spec.bins.slice() } : {}),
...(spec.os ? { os: spec.os.slice() } : {}),
...(spec.formula ? { formula: spec.formula } : {}),
...(spec.package ? { package: spec.package } : {}),
...(spec.module ? { module: spec.module } : {}),
...(spec.url ? { url: spec.url } : {}),
...(spec.archive ? { archive: spec.archive } : {}),
...(spec.extract !== undefined ? { extract: spec.extract } : {}),
...(spec.stripComponents !== undefined ? { stripComponents: spec.stripComponents } : {}),
...(spec.targetDir ? { targetDir: spec.targetDir } : {}),
};
}
function buildNodeInstallCommand(packageName: string, prefs: SkillsInstallPreferences): string[] {
switch (prefs.nodeManager) {
case "pnpm":
@ -465,11 +512,20 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
{
targetName: params.skillName,
targetType: "skill",
sourceDir: path.resolve(entry.skill.baseDir),
sourcePath: path.resolve(entry.skill.baseDir),
sourcePathKind: "directory",
source: skillSource,
builtinFindings: scanResult.findings,
request: {
kind: "skill-install",
mode: "install",
},
builtinScan: scanResult.builtinScan,
skill: {
installId: params.installId,
...(spec ? { installSpec: normalizeSkillInstallSpec(spec) } : {}),
},
},
{ source: skillSource, targetType: "skill" },
{ source: skillSource, targetType: "skill", requestKind: "skill-install" },
);
if (hookResult?.block) {
return {

View File

@ -15,6 +15,23 @@ type InstallScanFinding = {
message: string;
};
type BuiltinInstallScan = {
status: "ok" | "error";
scannedFiles: number;
critical: number;
warn: number;
info: number;
findings: InstallScanFinding[];
error?: string;
};
type PluginInstallRequestKind =
| "skill-install"
| "plugin-dir"
| "plugin-archive"
| "plugin-file"
| "plugin-npm";
export type InstallSecurityScanResult = {
blocked?: {
reason: string;
@ -30,14 +47,125 @@ function buildCriticalDetails(params: {
.join("; ");
}
function buildBuiltinScanFromError(error: unknown): BuiltinInstallScan {
return {
status: "error",
scannedFiles: 0,
critical: 0,
warn: 0,
info: 0,
findings: [],
error: String(error),
};
}
function buildBuiltinScanFromSummary(summary: {
scannedFiles: number;
critical: number;
warn: number;
info: number;
findings: InstallScanFinding[];
}): BuiltinInstallScan {
return {
status: "ok",
scannedFiles: summary.scannedFiles,
critical: summary.critical,
warn: summary.warn,
info: summary.info,
findings: summary.findings,
};
}
async function scanDirectoryTarget(params: {
includeFiles?: string[];
logger: InstallScanLogger;
path: string;
scanFailureMessage: string;
suspiciousMessage: string;
targetName: string;
warningMessage: string;
}): Promise<BuiltinInstallScan> {
try {
const scanSummary = await scanDirectoryWithSummary(params.path, {
includeFiles: params.includeFiles,
});
const builtinScan = buildBuiltinScanFromSummary(scanSummary);
if (scanSummary.critical > 0) {
params.logger.warn?.(
`${params.warningMessage}: ${buildCriticalDetails({ findings: scanSummary.findings })}`,
);
} else if (scanSummary.warn > 0) {
params.logger.warn?.(
params.suspiciousMessage
.replace("{count}", String(scanSummary.warn))
.replace("{target}", params.targetName),
);
}
return builtinScan;
} catch (err) {
params.logger.warn?.(params.scanFailureMessage.replace("{error}", String(err)));
return buildBuiltinScanFromError(err);
}
}
async function scanFileTarget(params: {
logger: InstallScanLogger;
path: string;
scanFailureMessage: string;
suspiciousMessage: string;
targetName: string;
warningMessage: string;
}): Promise<BuiltinInstallScan> {
const directory = path.dirname(params.path);
return await scanDirectoryTarget({
includeFiles: [params.path],
logger: params.logger,
path: directory,
scanFailureMessage: params.scanFailureMessage,
suspiciousMessage: params.suspiciousMessage,
targetName: params.targetName,
warningMessage: params.warningMessage,
});
}
async function runBeforeInstallHook(params: {
logger: InstallScanLogger;
installLabel: string;
source: string;
sourceDir: string;
sourcePath: string;
sourcePathKind: "file" | "directory";
targetName: string;
targetType: "skill" | "plugin";
builtinFindings: InstallScanFinding[];
requestKind: PluginInstallRequestKind;
requestMode: "install" | "update";
requestedSpecifier?: string;
builtinScan: BuiltinInstallScan;
skill?: {
installId: string;
installSpec?: {
id?: string;
kind: "brew" | "node" | "go" | "uv" | "download";
label?: string;
bins?: string[];
os?: string[];
formula?: string;
package?: string;
module?: string;
url?: string;
archive?: string;
extract?: boolean;
stripComponents?: number;
targetDir?: string;
};
};
plugin?: {
contentType: "bundle" | "package" | "file";
pluginId: string;
packageName?: string;
manifestId?: string;
version?: string;
extensions?: string[];
};
}): Promise<InstallSecurityScanResult | undefined> {
const hookRunner = getGlobalHookRunner();
if (!hookRunner?.hasHooks("before_install")) {
@ -50,10 +178,22 @@ async function runBeforeInstallHook(params: {
targetName: params.targetName,
targetType: params.targetType,
source: params.source,
sourceDir: params.sourceDir,
builtinFindings: params.builtinFindings,
sourcePath: params.sourcePath,
sourcePathKind: params.sourcePathKind,
request: {
kind: params.requestKind,
mode: params.requestMode,
...(params.requestedSpecifier ? { requestedSpecifier: params.requestedSpecifier } : {}),
},
builtinScan: params.builtinScan,
...(params.skill ? { skill: params.skill } : {}),
...(params.plugin ? { plugin: params.plugin } : {}),
},
{
source: params.source,
targetType: params.targetType,
requestKind: params.requestKind,
},
{ source: params.source, targetType: params.targetType },
);
if (hookResult?.block) {
const reason = hookResult.blockReason || "Installation blocked by plugin hook";
@ -80,34 +220,38 @@ export async function scanBundleInstallSourceRuntime(params: {
logger: InstallScanLogger;
pluginId: string;
sourceDir: string;
requestKind?: PluginInstallRequestKind;
requestedSpecifier?: string;
mode?: "install" | "update";
version?: 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 })}`,
);
} 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.`,
);
}
} catch (err) {
params.logger.warn?.(
`Bundle "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
}
const builtinScan = await scanDirectoryTarget({
logger: params.logger,
path: params.sourceDir,
scanFailureMessage: `Bundle "${params.pluginId}" code safety scan failed ({error}). Installation continues; run "openclaw security audit --deep" after install.`,
suspiciousMessage: `Bundle "{target}" has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
targetName: params.pluginId,
warningMessage: `WARNING: Bundle "${params.pluginId}" contains dangerous code patterns`,
});
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Bundle "${params.pluginId}" installation`,
source: "plugin-bundle",
sourceDir: params.sourceDir,
sourcePath: params.sourceDir,
sourcePathKind: "directory",
targetName: params.pluginId,
targetType: "plugin",
builtinFindings,
requestKind: params.requestKind ?? "plugin-dir",
requestMode: params.mode ?? "install",
requestedSpecifier: params.requestedSpecifier,
builtinScan,
plugin: {
contentType: "bundle",
pluginId: params.pluginId,
manifestId: params.pluginId,
...(params.version ? { version: params.version } : {}),
},
});
}
@ -116,6 +260,12 @@ export async function scanPackageInstallSourceRuntime(params: {
logger: InstallScanLogger;
packageDir: string;
pluginId: string;
requestKind?: PluginInstallRequestKind;
requestedSpecifier?: string;
mode?: "install" | "update";
packageName?: string;
manifestId?: string;
version?: string;
}): Promise<InstallSecurityScanResult | undefined> {
const forcedScanEntries: string[] = [];
for (const entry of params.extensions) {
@ -134,34 +284,71 @@ 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 })}`,
);
} 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.`,
);
}
} catch (err) {
params.logger.warn?.(
`Plugin "${params.pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
}
const builtinScan = await scanDirectoryTarget({
includeFiles: forcedScanEntries,
logger: params.logger,
path: params.packageDir,
scanFailureMessage: `Plugin "${params.pluginId}" code safety scan failed ({error}). Installation continues; run "openclaw security audit --deep" after install.`,
suspiciousMessage: `Plugin "{target}" has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
targetName: params.pluginId,
warningMessage: `WARNING: Plugin "${params.pluginId}" contains dangerous code patterns`,
});
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Plugin "${params.pluginId}" installation`,
source: "plugin-package",
sourceDir: params.packageDir,
sourcePath: params.packageDir,
sourcePathKind: "directory",
targetName: params.pluginId,
targetType: "plugin",
builtinFindings,
requestKind: params.requestKind ?? "plugin-dir",
requestMode: params.mode ?? "install",
requestedSpecifier: params.requestedSpecifier,
builtinScan,
plugin: {
contentType: "package",
pluginId: params.pluginId,
...(params.packageName ? { packageName: params.packageName } : {}),
...(params.manifestId ? { manifestId: params.manifestId } : {}),
...(params.version ? { version: params.version } : {}),
extensions: params.extensions.slice(),
},
});
}
export async function scanFileInstallSourceRuntime(params: {
filePath: string;
logger: InstallScanLogger;
mode?: "install" | "update";
pluginId: string;
requestedSpecifier?: string;
}): Promise<InstallSecurityScanResult | undefined> {
const builtinScan = await scanFileTarget({
logger: params.logger,
path: params.filePath,
scanFailureMessage: `Plugin file "${params.pluginId}" code safety scan failed ({error}). Installation continues; run "openclaw security audit --deep" after install.`,
suspiciousMessage: `Plugin file "{target}" has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
targetName: params.pluginId,
warningMessage: `WARNING: Plugin file "${params.pluginId}" contains dangerous code patterns`,
});
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Plugin file "${params.pluginId}" installation`,
source: "plugin-file",
sourcePath: params.filePath,
sourcePathKind: "file",
targetName: params.pluginId,
targetType: "plugin",
requestKind: "plugin-file",
requestMode: params.mode ?? "install",
requestedSpecifier: params.requestedSpecifier,
builtinScan,
plugin: {
contentType: "file",
pluginId: params.pluginId,
extensions: [path.basename(params.filePath)],
},
});
}

View File

@ -8,6 +8,12 @@ export type InstallSecurityScanResult = {
};
};
export type PluginInstallRequestKind =
| "plugin-dir"
| "plugin-archive"
| "plugin-file"
| "plugin-npm";
async function loadInstallSecurityScanRuntime() {
return await import("./install-security-scan.runtime.js");
}
@ -16,6 +22,10 @@ export async function scanBundleInstallSource(params: {
logger: InstallScanLogger;
pluginId: string;
sourceDir: string;
requestKind?: PluginInstallRequestKind;
requestedSpecifier?: string;
mode?: "install" | "update";
version?: string;
}): Promise<InstallSecurityScanResult | undefined> {
const { scanBundleInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
return await scanBundleInstallSourceRuntime(params);
@ -26,7 +36,24 @@ export async function scanPackageInstallSource(params: {
logger: InstallScanLogger;
packageDir: string;
pluginId: string;
requestKind?: PluginInstallRequestKind;
requestedSpecifier?: string;
mode?: "install" | "update";
packageName?: string;
manifestId?: string;
version?: string;
}): Promise<InstallSecurityScanResult | undefined> {
const { scanPackageInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
return await scanPackageInstallSourceRuntime(params);
}
export async function scanFileInstallSource(params: {
filePath: string;
logger: InstallScanLogger;
mode?: "install" | "update";
pluginId: string;
requestedSpecifier?: string;
}): Promise<InstallSecurityScanResult | undefined> {
const { scanFileInstallSourceRuntime } = await loadInstallSecurityScanRuntime();
return await scanFileInstallSourceRuntime(params);
}

View File

@ -22,7 +22,11 @@ import {
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { resolveCompatibilityHostVersion, resolveRuntimeServiceVersion } from "../version.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
import {
scanBundleInstallSource,
scanFileInstallSource,
scanPackageInstallSource,
} from "./install-security-scan.js";
import {
getPackageManifestMetadata,
loadPluginManifest,
@ -56,6 +60,7 @@ export {
resolveRuntimeServiceVersion,
resolveTimedInstallModeOptions,
scanBundleInstallSource,
scanFileInstallSource,
scanPackageInstallSource,
validateRegistryNpmSpec,
withExtractedArchiveRoot,

View File

@ -73,6 +73,11 @@ export type PluginNpmIntegrityDriftParams = {
resolution: NpmSpecResolution;
};
type PluginInstallPolicyRequest = {
kind: "plugin-dir" | "plugin-archive" | "plugin-file" | "plugin-npm";
requestedSpecifier?: string;
};
const defaultLogger: PluginInstallLogger = {};
function safeFileName(input: string): string {
return safeDirName(input);
@ -214,11 +219,12 @@ type PackageInstallCommonParams = {
mode?: "install" | "update";
dryRun?: boolean;
expectedPluginId?: string;
installPolicyRequest?: PluginInstallPolicyRequest;
};
type FileInstallCommonParams = Pick<
PackageInstallCommonParams,
"extensionsDir" | "logger" | "mode" | "dryRun"
"extensionsDir" | "logger" | "mode" | "dryRun" | "installPolicyRequest"
>;
function pickPackageInstallCommonParams(
@ -231,6 +237,7 @@ function pickPackageInstallCommonParams(
mode: params.mode,
dryRun: params.dryRun,
expectedPluginId: params.expectedPluginId,
installPolicyRequest: params.installPolicyRequest,
};
}
@ -240,6 +247,7 @@ function pickFileInstallCommonParams(params: FileInstallCommonParams): FileInsta
logger: params.logger,
mode: params.mode,
dryRun: params.dryRun,
installPolicyRequest: params.installPolicyRequest,
};
}
@ -380,6 +388,10 @@ async function installBundleFromSourceDir(
sourceDir: params.sourceDir,
pluginId,
logger,
requestKind: params.installPolicyRequest?.kind,
requestedSpecifier: params.installPolicyRequest?.requestedSpecifier,
mode,
version: manifestRes.manifest.version,
});
if (scanResult?.blocked) {
return { ok: false, error: scanResult.blocked.reason };
@ -553,6 +565,12 @@ async function installPluginFromPackageDir(
pluginId,
logger,
extensions,
requestKind: params.installPolicyRequest?.kind,
requestedSpecifier: params.installPolicyRequest?.requestedSpecifier,
mode,
packageName: pkgName || undefined,
manifestId: manifestPluginId,
version: typeof manifest.version === "string" ? manifest.version : undefined,
});
if (scanResult?.blocked) {
return { ok: false, error: scanResult.blocked.reason };
@ -603,6 +621,10 @@ export async function installPluginFromArchive(
const logger = params.logger ?? defaultLogger;
const timeoutMs = params.timeoutMs ?? 120_000;
const mode = params.mode ?? "install";
const installPolicyRequest = params.installPolicyRequest ?? {
kind: "plugin-archive",
requestedSpecifier: params.archivePath,
};
const archivePathResult = await runtime.resolveArchiveSourcePath(params.archivePath);
if (!archivePathResult.ok) {
return archivePathResult;
@ -625,6 +647,7 @@ export async function installPluginFromArchive(
mode,
dryRun: params.dryRun,
expectedPluginId: params.expectedPluginId,
installPolicyRequest,
}),
}),
});
@ -637,6 +660,10 @@ export async function installPluginFromDir(
): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const dirPath = resolveUserPath(params.dirPath);
const installPolicyRequest = params.installPolicyRequest ?? {
kind: "plugin-dir",
requestedSpecifier: params.dirPath,
};
if (!(await runtime.fileExists(dirPath))) {
return { ok: false, error: `directory not found: ${dirPath}` };
}
@ -647,7 +674,10 @@ export async function installPluginFromDir(
return await installPluginFromSourceDir({
sourceDir: dirPath,
...pickPackageInstallCommonParams(params),
...pickPackageInstallCommonParams({
...params,
installPolicyRequest,
}),
});
}
@ -657,11 +687,16 @@ export async function installPluginFromFile(params: {
logger?: PluginInstallLogger;
mode?: "install" | "update";
dryRun?: boolean;
installPolicyRequest?: PluginInstallPolicyRequest;
}): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const { logger, mode, dryRun } = runtime.resolveInstallModeOptions(params, defaultLogger);
const filePath = resolveUserPath(params.filePath);
const installPolicyRequest = params.installPolicyRequest ?? {
kind: "plugin-file",
requestedSpecifier: params.filePath,
};
if (!(await runtime.fileExists(filePath))) {
return { ok: false, error: `file not found: ${filePath}` };
}
@ -692,6 +727,23 @@ export async function installPluginFromFile(params: {
return buildFileInstallResult(pluginId, targetFile);
}
try {
const scanResult = await runtime.scanFileInstallSource({
filePath,
logger,
mode,
pluginId,
requestedSpecifier: installPolicyRequest.requestedSpecifier,
});
if (scanResult?.blocked) {
return { ok: false, error: scanResult.blocked.reason };
}
} catch (err) {
logger.warn?.(
`Plugin file "${pluginId}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
);
}
logger.info?.(`Installing to ${targetFile}`);
try {
await runtime.writeFileFromPathWithinRoot({
@ -734,6 +786,10 @@ export async function installPluginFromNpmSpec(params: {
}
logger.info?.(`Downloading ${spec}`);
const installPolicyRequest: PluginInstallPolicyRequest = {
kind: "plugin-npm",
requestedSpecifier: spec,
};
const flowResult = await runtime.installFromNpmSpecArchiveWithInstaller({
tempDirPrefix: "openclaw-npm-pack-",
spec,
@ -751,6 +807,7 @@ export async function installPluginFromNpmSpec(params: {
mode,
dryRun,
expectedPluginId,
installPolicyRequest,
},
});
const finalized = runtime.finalizeNpmSpecArchiveInstall(flowResult);
@ -781,6 +838,10 @@ export async function installPluginFromPath(
return await installPluginFromDir({
dirPath: resolved,
...packageInstallOptions,
installPolicyRequest: {
kind: "plugin-dir",
requestedSpecifier: params.path,
},
});
}
@ -789,11 +850,21 @@ export async function installPluginFromPath(
return await installPluginFromArchive({
archivePath: resolved,
...packageInstallOptions,
installPolicyRequest: {
kind: "plugin-archive",
requestedSpecifier: params.path,
},
});
}
return await installPluginFromFile({
filePath: resolved,
...pickFileInstallCommonParams(params),
...pickFileInstallCommonParams({
...params,
installPolicyRequest: {
kind: "plugin-file",
requestedSpecifier: params.path,
},
}),
});
}

View File

@ -2340,11 +2340,82 @@ export type PluginHookGatewayStopEvent = {
};
export type PluginInstallTargetType = "skill" | "plugin";
export type PluginInstallRequestKind =
| "skill-install"
| "plugin-dir"
| "plugin-archive"
| "plugin-file"
| "plugin-npm";
export type PluginInstallSourcePathKind = "file" | "directory";
export type PluginInstallFinding = {
ruleId: string;
severity: "info" | "warn" | "critical";
file: string;
line: number;
message: string;
};
export type PluginHookBeforeInstallRequest = {
/** Original install entrypoint/provenance. */
kind: PluginInstallRequestKind;
/** Install mode requested by the caller. */
mode: "install" | "update";
/** Raw user-facing specifier or path when available. */
requestedSpecifier?: string;
};
export type PluginHookBeforeInstallBuiltinScan = {
/** Whether the built-in scan completed successfully. */
status: "ok" | "error";
/** Number of files the built-in scanner actually inspected. */
scannedFiles: number;
critical: number;
warn: number;
info: number;
findings: PluginInstallFinding[];
/** Scanner failure reason when status=`error`. */
error?: string;
};
export type PluginHookBeforeInstallSkillInstallSpec = {
id?: string;
kind: "brew" | "node" | "go" | "uv" | "download";
label?: string;
bins?: string[];
os?: string[];
formula?: string;
package?: string;
module?: string;
url?: string;
archive?: string;
extract?: boolean;
stripComponents?: number;
targetDir?: string;
};
export type PluginHookBeforeInstallSkill = {
installId: string;
installSpec?: PluginHookBeforeInstallSkillInstallSpec;
};
export type PluginHookBeforeInstallPlugin = {
/** Canonical plugin id OpenClaw will install under. */
pluginId: string;
/** Normalized installable content shape after source resolution. */
contentType: "bundle" | "package" | "file";
packageName?: string;
manifestId?: string;
version?: string;
extensions?: string[];
};
// before_install hook
export type PluginHookBeforeInstallContext = {
/** Category of install target being checked. */
targetType: PluginInstallTargetType;
/** Original install entrypoint/provenance. */
requestKind: PluginInstallRequestKind;
/** Origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
source?: string;
};
@ -2354,29 +2425,25 @@ export type PluginHookBeforeInstallEvent = {
targetType: PluginInstallTargetType;
/** Human-readable skill or plugin name. */
targetName: string;
/** Absolute path to the install target source directory being scanned. */
sourceDir: string;
/** Absolute path to the install target content being scanned. */
sourcePath: string;
/** Whether the install target content is a file or directory. */
sourcePathKind: PluginInstallSourcePathKind;
/** 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;
}>;
/** Install request provenance and caller mode. */
request: PluginHookBeforeInstallRequest;
/** Structured result of the built-in scanner. */
builtinScan: PluginHookBeforeInstallBuiltinScan;
/** Present when targetType=`skill`. */
skill?: PluginHookBeforeInstallSkill;
/** Present when targetType=`plugin`. */
plugin?: PluginHookBeforeInstallPlugin;
};
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;
}>;
findings?: PluginInstallFinding[];
/** If true, block the installation entirely. */
block?: boolean;
/** Human-readable reason for blocking. */