mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-01 20:31:19 +00:00
plugins: enrich before_install policy context
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user