refactor(plugins): centralize before_install context shaping

This commit is contained in:
George Zhang
2026-03-29 11:01:24 -07:00
parent 9a07fd83fb
commit 2607191d04
8 changed files with 104 additions and 57 deletions

View File

@@ -511,7 +511,7 @@ Event fields:
- **`targetName`**: Human-readable skill name or plugin id for the install target
- **`sourcePath`**: Absolute path to the install target content being scanned
- **`sourcePathKind`**: Whether the scanned content is a `file` or `directory`
- **`source`**: Normalized install origin when available (for example `openclaw-bundled`, `openclaw-workspace`, `plugin-bundle`, `plugin-package`, or `plugin-file`)
- **`origin`**: Normalized install origin when available (for example `openclaw-bundled`, `openclaw-workspace`, `plugin-bundle`, `plugin-package`, or `plugin-file`)
- **`request`**: Provenance for the install request, including `kind`, `mode`, and optional `requestedSpecifier`
- **`builtinScan`**: Structured result of the built-in scanner, including `status`, summary counts, findings, and optional `error`
- **`skill`**: Skill install metadata when `targetType` is `skill`, including `installId` and the selected `installSpec`

View File

@@ -169,7 +169,7 @@ describe("installSkill code safety scanning", () => {
expect(handler.mock.calls[0]?.[0]).toMatchObject({
targetName: "policy-skill",
targetType: "skill",
source: "openclaw-workspace",
origin: "openclaw-workspace",
sourcePath: expect.stringContaining("policy-skill"),
sourcePathKind: "directory",
request: {
@@ -189,7 +189,7 @@ describe("installSkill code safety scanning", () => {
},
});
expect(handler.mock.calls[0]?.[1]).toEqual({
source: "openclaw-workspace",
origin: "openclaw-workspace",
targetType: "skill",
requestKind: "skill-install",
});

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveBrewExecutable } from "../infra/brew.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { createBeforeInstallHookPayload } from "../plugins/install-policy-context.js";
import { runCommandWithTimeout, type CommandOptions } from "../process/exec.js";
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
import { resolveUserPath } from "../utils.js";
@@ -508,25 +509,23 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("before_install")) {
try {
const hookResult = await hookRunner.runBeforeInstall(
{
targetName: params.skillName,
targetType: "skill",
sourcePath: path.resolve(entry.skill.baseDir),
sourcePathKind: "directory",
source: skillSource,
request: {
kind: "skill-install",
mode: "install",
},
builtinScan: scanResult.builtinScan,
skill: {
installId: params.installId,
...(spec ? { installSpec: normalizeSkillInstallSpec(spec) } : {}),
},
const { event, ctx } = createBeforeInstallHookPayload({
targetName: params.skillName,
targetType: "skill",
origin: skillSource,
sourcePath: path.resolve(entry.skill.baseDir),
sourcePathKind: "directory",
request: {
kind: "skill-install",
mode: "install",
},
{ source: skillSource, targetType: "skill", requestKind: "skill-install" },
);
builtinScan: scanResult.builtinScan,
skill: {
installId: params.installId,
...(spec ? { installSpec: normalizeSkillInstallSpec(spec) } : {}),
},
});
const hookResult = await hookRunner.runBeforeInstall(event, ctx);
if (hookResult?.block) {
return {
ok: false,

View File

@@ -27,7 +27,7 @@ function addBeforeInstallHook(
}
const stubCtx: PluginHookBeforeInstallContext = {
source: "openclaw-workspace",
origin: "openclaw-workspace",
targetType: "skill",
requestKind: "skill-install",
};
@@ -37,7 +37,7 @@ const stubEvent: PluginHookBeforeInstallEvent = {
targetType: "skill",
sourcePath: "/tmp/demo-skill",
sourcePathKind: "directory",
source: "openclaw-workspace",
origin: "openclaw-workspace",
request: {
kind: "skill-install",
mode: "install",

View File

@@ -0,0 +1,53 @@
import type {
PluginHookBeforeInstallBuiltinScan,
PluginHookBeforeInstallContext,
PluginHookBeforeInstallEvent,
PluginHookBeforeInstallPlugin,
PluginHookBeforeInstallRequest,
PluginHookBeforeInstallSkill,
PluginInstallSourcePathKind,
PluginInstallTargetType,
} from "./types.js";
/**
* Centralized builder for the public before_install hook contract.
*
* Keep all payload shaping here so partner feedback lands in one place instead
* of drifting across individual install codepaths.
*/
export type BeforeInstallHookPayloadParams = {
targetType: PluginInstallTargetType;
targetName: string;
origin?: string;
sourcePath: string;
sourcePathKind: PluginInstallSourcePathKind;
request: PluginHookBeforeInstallRequest;
builtinScan: PluginHookBeforeInstallBuiltinScan;
skill?: PluginHookBeforeInstallSkill;
plugin?: PluginHookBeforeInstallPlugin;
};
export function createBeforeInstallHookPayload(params: BeforeInstallHookPayloadParams): {
ctx: PluginHookBeforeInstallContext;
event: PluginHookBeforeInstallEvent;
} {
const event: PluginHookBeforeInstallEvent = {
targetType: params.targetType,
targetName: params.targetName,
sourcePath: params.sourcePath,
sourcePathKind: params.sourcePathKind,
...(params.origin ? { origin: params.origin } : {}),
request: params.request,
builtinScan: params.builtinScan,
...(params.skill ? { skill: params.skill } : {}),
...(params.plugin ? { plugin: params.plugin } : {}),
};
const ctx: PluginHookBeforeInstallContext = {
targetType: params.targetType,
requestKind: params.request.kind,
...(params.origin ? { origin: params.origin } : {}),
};
return { event, ctx };
}

View File

@@ -2,6 +2,7 @@ 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";
import { createBeforeInstallHookPayload } from "./install-policy-context.js";
type InstallScanLogger = {
warn?: (message: string) => void;
@@ -131,7 +132,7 @@ async function scanFileTarget(params: {
async function runBeforeInstallHook(params: {
logger: InstallScanLogger;
installLabel: string;
source: string;
origin: string;
sourcePath: string;
sourcePathKind: "file" | "directory";
targetName: string;
@@ -173,28 +174,22 @@ async function runBeforeInstallHook(params: {
}
try {
const hookResult = await hookRunner.runBeforeInstall(
{
targetName: params.targetName,
targetType: params.targetType,
source: params.source,
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 } : {}),
const { event, ctx } = createBeforeInstallHookPayload({
targetName: params.targetName,
targetType: params.targetType,
origin: params.origin,
sourcePath: params.sourcePath,
sourcePathKind: params.sourcePathKind,
request: {
kind: params.requestKind,
mode: params.requestMode,
...(params.requestedSpecifier ? { requestedSpecifier: params.requestedSpecifier } : {}),
},
{
source: params.source,
targetType: params.targetType,
requestKind: params.requestKind,
},
);
builtinScan: params.builtinScan,
...(params.skill ? { skill: params.skill } : {}),
...(params.plugin ? { plugin: params.plugin } : {}),
});
const hookResult = await hookRunner.runBeforeInstall(event, ctx);
if (hookResult?.block) {
const reason = hookResult.blockReason || "Installation blocked by plugin hook";
params.logger.warn?.(`WARNING: ${params.installLabel} blocked by plugin hook: ${reason}`);
@@ -237,7 +232,7 @@ export async function scanBundleInstallSourceRuntime(params: {
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Bundle "${params.pluginId}" installation`,
source: "plugin-bundle",
origin: "plugin-bundle",
sourcePath: params.sourceDir,
sourcePathKind: "directory",
targetName: params.pluginId,
@@ -297,7 +292,7 @@ export async function scanPackageInstallSourceRuntime(params: {
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Plugin "${params.pluginId}" installation`,
source: "plugin-package",
origin: "plugin-package",
sourcePath: params.packageDir,
sourcePathKind: "directory",
targetName: params.pluginId,
@@ -336,7 +331,7 @@ export async function scanFileInstallSourceRuntime(params: {
return await runBeforeInstallHook({
logger: params.logger,
installLabel: `Plugin file "${params.pluginId}" installation`,
source: "plugin-file",
origin: "plugin-file",
sourcePath: params.filePath,
sourcePathKind: "file",
targetName: params.pluginId,

View File

@@ -777,7 +777,7 @@ describe("installPluginFromArchive", () => {
expect(handler.mock.calls[0]?.[0]).toMatchObject({
targetName: "hook-findings-plugin",
targetType: "plugin",
source: "plugin-package",
origin: "plugin-package",
sourcePath: pluginDir,
sourcePathKind: "directory",
request: {
@@ -797,7 +797,7 @@ describe("installPluginFromArchive", () => {
},
});
expect(handler.mock.calls[0]?.[1]).toEqual({
source: "plugin-package",
origin: "plugin-package",
targetType: "plugin",
requestKind: "plugin-dir",
});
@@ -840,7 +840,7 @@ describe("installPluginFromArchive", () => {
expect(handler.mock.calls[0]?.[0]).toMatchObject({
targetName: "dangerous-blocked-plugin",
targetType: "plugin",
source: "plugin-package",
origin: "plugin-package",
request: {
kind: "plugin-dir",
mode: "install",
@@ -1202,7 +1202,7 @@ describe("installPluginFromPath", () => {
expect(handler.mock.calls[0]?.[0]).toMatchObject({
targetName: "payload",
targetType: "plugin",
source: "plugin-file",
origin: "plugin-file",
sourcePath,
sourcePathKind: "file",
request: {
@@ -1220,7 +1220,7 @@ describe("installPluginFromPath", () => {
},
});
expect(handler.mock.calls[0]?.[1]).toEqual({
source: "plugin-file",
origin: "plugin-file",
targetType: "plugin",
requestKind: "plugin-file",
});

View File

@@ -2416,8 +2416,8 @@ export type PluginHookBeforeInstallContext = {
targetType: PluginInstallTargetType;
/** Original install entrypoint/provenance. */
requestKind: PluginInstallRequestKind;
/** Origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
source?: string;
/** Normalized origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
origin?: string;
};
export type PluginHookBeforeInstallEvent = {
@@ -2429,8 +2429,8 @@ export type PluginHookBeforeInstallEvent = {
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;
/** Normalized origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */
origin?: string;
/** Install request provenance and caller mode. */
request: PluginHookBeforeInstallRequest;
/** Structured result of the built-in scanner. */