mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
refactor(plugins): centralize before_install context shaping
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
53
src/plugins/install-policy-context.ts
Normal file
53
src/plugins/install-policy-context.ts
Normal 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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user