mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 17:02:46 +00:00
Plugins: extract hook compatibility
This commit is contained in:
@@ -75,6 +75,7 @@ This is an implementation checklist, not a future-design spec.
|
||||
| Context-engine registration writes | `src/plugins/registry.ts` | host-owned context-engine registry | `partial` | Context-engine id normalization now delegates to `src/extension-host/runtime-registrations.ts`, and the compatibility write now routes through `src/extension-host/registry-writes.ts`; the actual context-engine registry remains legacy-owned. |
|
||||
| Legacy hook registration writes | `src/plugins/registry.ts` | host-owned hook registry | `partial` | Hook-entry construction and event normalization now delegate to `src/extension-host/runtime-registrations.ts`, and the compatibility write now routes through `src/extension-host/registry-writes.ts`; internal-hook bridging still remains in the legacy plugin registry. |
|
||||
| Typed-hook registration writes | `src/plugins/registry.ts` | host-owned typed-hook registry | `partial` | Typed-hook record construction and hook-name validation now delegate to `src/extension-host/runtime-registrations.ts`, and the compatibility write now routes through `src/extension-host/registry-writes.ts`; prompt-injection policy and execution semantics remain legacy-owned. |
|
||||
| Hook compatibility policy and bridging | `src/plugins/registry.ts` | `src/extension-host/hook-compat.ts` | `partial` | Legacy internal-hook bridging and typed prompt-injection compatibility policy now delegate through a host-owned helper; actual hook execution ownership remains legacy-owned. |
|
||||
| Hook execution and global runner | `src/plugins/hook-runner-global.ts`, `src/hooks/internal-hooks.ts`, plugin hook registration in `src/plugins/registry.ts` | canonical kernel event stages + host bridges | `not started` | No canonical event-stage migration has landed yet. |
|
||||
| Service lifecycle | `src/plugins/services.ts` and plugin service registration | extension host lifecycle | `not started` | Service startup and teardown still depend on legacy plugin registry/service ownership. |
|
||||
| CLI registration | plugin CLI registration in `src/plugins/registry.ts` and CLI loaders | extension host registry + static descriptors where possible | `not started` | No host-owned CLI registry exists yet. |
|
||||
|
||||
66
src/extension-host/hook-compat.test.ts
Normal file
66
src/extension-host/hook-compat.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
applyExtensionHostTypedHookPolicy,
|
||||
bridgeExtensionHostLegacyHooks,
|
||||
constrainExtensionHostPromptInjectionHook,
|
||||
} from "./hook-compat.js";
|
||||
|
||||
describe("extension host hook compatibility", () => {
|
||||
it("bridges legacy hooks only when internal hook registration is enabled", () => {
|
||||
const registerHook = vi.fn();
|
||||
|
||||
bridgeExtensionHostLegacyHooks({
|
||||
events: ["before_send", "after_send"],
|
||||
handler: (() => {}) as never,
|
||||
hookSystemEnabled: true,
|
||||
registerHook: registerHook as never,
|
||||
});
|
||||
|
||||
expect(registerHook).toHaveBeenCalledTimes(2);
|
||||
expect(registerHook).toHaveBeenNthCalledWith(1, "before_send", expect.any(Function));
|
||||
expect(registerHook).toHaveBeenNthCalledWith(2, "after_send", expect.any(Function));
|
||||
});
|
||||
|
||||
it("constrains prompt-mutation fields for before_agent_start hooks", async () => {
|
||||
const handler = vi.fn(async () => ({
|
||||
messages: [{ role: "system", content: "keep" }],
|
||||
systemPrompt: "drop",
|
||||
prependContext: "drop",
|
||||
appendSystemContext: "drop",
|
||||
}));
|
||||
|
||||
const constrained = constrainExtensionHostPromptInjectionHook(handler as never);
|
||||
const result = await constrained({} as never, {} as never);
|
||||
|
||||
expect(result).toEqual({
|
||||
messages: [{ role: "system", content: "keep" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks before_prompt_build and constrains before_agent_start when prompt injection is disabled", () => {
|
||||
const blocked = applyExtensionHostTypedHookPolicy({
|
||||
hookName: "before_prompt_build",
|
||||
handler: (() => ({})) as never,
|
||||
policy: { allowPromptInjection: false },
|
||||
blockedMessage: "blocked",
|
||||
constrainedMessage: "constrained",
|
||||
});
|
||||
const constrained = applyExtensionHostTypedHookPolicy({
|
||||
hookName: "before_agent_start",
|
||||
handler: (() => ({})) as never,
|
||||
policy: { allowPromptInjection: false },
|
||||
blockedMessage: "blocked",
|
||||
constrainedMessage: "constrained",
|
||||
});
|
||||
|
||||
expect(blocked).toEqual({
|
||||
ok: false,
|
||||
message: "blocked",
|
||||
});
|
||||
expect(constrained.ok).toBe(true);
|
||||
if (constrained.ok) {
|
||||
expect(constrained.warningMessage).toBe("constrained");
|
||||
expect(constrained.entryHandler).toBeTypeOf("function");
|
||||
}
|
||||
});
|
||||
});
|
||||
91
src/extension-host/hook-compat.ts
Normal file
91
src/extension-host/hook-compat.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { registerInternalHook, type InternalHookHandler } from "../hooks/internal-hooks.js";
|
||||
import type {
|
||||
PluginHookHandlerMap,
|
||||
PluginHookName,
|
||||
PluginHookRegistration as TypedPluginHookRegistration,
|
||||
} from "../plugins/types.js";
|
||||
import {
|
||||
isPromptInjectionHookName,
|
||||
stripPromptMutationFieldsFromLegacyHookResult,
|
||||
} from "../plugins/types.js";
|
||||
|
||||
export function constrainExtensionHostPromptInjectionHook(
|
||||
handler: PluginHookHandlerMap["before_agent_start"],
|
||||
): PluginHookHandlerMap["before_agent_start"] {
|
||||
return (event, ctx) => {
|
||||
const result = handler(event, ctx);
|
||||
if (result && typeof result === "object" && "then" in result) {
|
||||
return Promise.resolve(result).then((resolved) =>
|
||||
stripPromptMutationFieldsFromLegacyHookResult(resolved),
|
||||
);
|
||||
}
|
||||
return stripPromptMutationFieldsFromLegacyHookResult(result);
|
||||
};
|
||||
}
|
||||
|
||||
export function bridgeExtensionHostLegacyHooks(params: {
|
||||
events: string[];
|
||||
handler: InternalHookHandler;
|
||||
hookSystemEnabled: boolean;
|
||||
register?: boolean;
|
||||
registerHook?: typeof registerInternalHook;
|
||||
}): void {
|
||||
if (!params.hookSystemEnabled || params.register === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const registerHook = params.registerHook ?? registerInternalHook;
|
||||
for (const event of params.events) {
|
||||
registerHook(event, params.handler);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyExtensionHostTypedHookPolicy<K extends PluginHookName>(params: {
|
||||
hookName: K;
|
||||
handler: PluginHookHandlerMap[K];
|
||||
policy?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
blockedMessage: string;
|
||||
constrainedMessage: string;
|
||||
}):
|
||||
| {
|
||||
ok: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
ok: true;
|
||||
entryHandler: TypedPluginHookRegistration["handler"];
|
||||
warningMessage?: string;
|
||||
} {
|
||||
if (
|
||||
!(params.policy?.allowPromptInjection === false && isPromptInjectionHookName(params.hookName))
|
||||
) {
|
||||
return {
|
||||
ok: true,
|
||||
entryHandler: params.handler,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.hookName === "before_prompt_build") {
|
||||
return {
|
||||
ok: false,
|
||||
message: params.blockedMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.hookName === "before_agent_start") {
|
||||
return {
|
||||
ok: true,
|
||||
entryHandler: constrainExtensionHostPromptInjectionHook(
|
||||
params.handler as PluginHookHandlerMap["before_agent_start"],
|
||||
),
|
||||
warningMessage: params.constrainedMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
entryHandler: params.handler,
|
||||
};
|
||||
}
|
||||
@@ -179,20 +179,6 @@ type PluginTypedHookPolicy = {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
|
||||
const constrainLegacyPromptInjectionHook = (
|
||||
handler: PluginHookHandlerMap["before_agent_start"],
|
||||
): PluginHookHandlerMap["before_agent_start"] => {
|
||||
return (event, ctx) => {
|
||||
const result = handler(event, ctx);
|
||||
if (result && typeof result === "object" && "then" in result) {
|
||||
return Promise.resolve(result).then((resolved) =>
|
||||
stripPromptMutationFieldsFromLegacyHookResult(resolved),
|
||||
);
|
||||
}
|
||||
return stripPromptMutationFieldsFromLegacyHookResult(result);
|
||||
};
|
||||
};
|
||||
|
||||
export function createEmptyPluginRegistry(): PluginRegistry {
|
||||
return {
|
||||
plugins: [],
|
||||
@@ -320,14 +306,13 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
events: normalized.events,
|
||||
});
|
||||
|
||||
const hookSystemEnabled = config?.hooks?.internal?.enabled === true;
|
||||
if (!hookSystemEnabled || opts?.register === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of normalized.events) {
|
||||
registerInternalHook(event, handler);
|
||||
}
|
||||
bridgeExtensionHostLegacyHooks({
|
||||
events: normalized.events,
|
||||
handler,
|
||||
hookSystemEnabled: config?.hooks?.internal?.enabled === true,
|
||||
register: opts?.register,
|
||||
registerHook: registerInternalHook,
|
||||
});
|
||||
};
|
||||
|
||||
const registerGatewayMethod = (
|
||||
@@ -620,28 +605,29 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
let effectiveHandler = normalized.entry.handler;
|
||||
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(normalized.hookName)) {
|
||||
if (normalized.hookName === "before_prompt_build") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${normalized.hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (normalized.hookName === "before_agent_start") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${normalized.hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
effectiveHandler = constrainLegacyPromptInjectionHook(
|
||||
handler as PluginHookHandlerMap["before_agent_start"],
|
||||
) as PluginHookHandlerMap[K];
|
||||
}
|
||||
const policyResult = applyExtensionHostTypedHookPolicy({
|
||||
hookName: normalized.hookName,
|
||||
handler,
|
||||
policy,
|
||||
blockedMessage: `typed hook "${normalized.hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
constrainedMessage: `typed hook "${normalized.hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
if (!policyResult.ok) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: policyResult.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (policyResult.warningMessage) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: policyResult.warningMessage,
|
||||
});
|
||||
}
|
||||
addExtensionTypedHookRegistration({
|
||||
registry,
|
||||
@@ -650,7 +636,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
...normalized.entry,
|
||||
pluginId: record.id,
|
||||
hookName: normalized.hookName,
|
||||
handler: effectiveHandler,
|
||||
handler: policyResult.entryHandler,
|
||||
} as TypedPluginHookRegistration,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user