mirror of https://github.com/openclaw/openclaw.git
fix: preserve legacy overrides in hook policy
This commit is contained in:
parent
f14bff643c
commit
6aefd1c0e8
|
|
@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
|||
- TTS/OpenAI-compatible endpoints: add `messages.tts.openai.baseUrl` config support with config-over-env precedence, endpoint-aware directive validation, and OpenAI TTS request routing to the resolved base URL. (#34321) thanks @RealKai42.
|
||||
- Plugins/before_prompt_build system-context fields: add `prependSystemContext` and `appendSystemContext` so static plugin guidance can be placed in system prompt space for provider caching and lower repeated prompt token cost. (#35177) thanks @maweibin.
|
||||
- Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails. (#35094) Thanks @joshavant.
|
||||
- Plugins/hook policy: add `plugins.entries.<id>.hooks.allowPromptInjection`, validate unknown typed hook names at runtime, and preserve legacy `before_agent_start` model/provider overrides while stripping prompt-mutating fields when prompt injection is disabled. (#36567) thanks @gumadeiras.
|
||||
|
||||
### Breaking
|
||||
|
||||
|
|
|
|||
|
|
@ -2310,7 +2310,7 @@ See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.5 via LM Studio
|
|||
- `allow`: optional allowlist (only listed plugins load). `deny` wins.
|
||||
- `plugins.entries.<id>.apiKey`: plugin-level API key convenience field (when supported by the plugin).
|
||||
- `plugins.entries.<id>.env`: plugin-scoped env var map.
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks this plugin from registering prompt-mutating typed hooks (`before_prompt_build`, `before_agent_start`).
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks `before_prompt_build` and ignores prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride`.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by plugin schema).
|
||||
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
|
||||
- `plugins.installs`: CLI-managed install metadata used by `openclaw plugins update`.
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ Important hooks for prompt construction:
|
|||
Core-enforced hook policy:
|
||||
|
||||
- Operators can disable prompt mutation hooks per plugin via `plugins.entries.<id>.hooks.allowPromptInjection: false`.
|
||||
- When disabled, OpenClaw blocks registration of `before_prompt_build` and `before_agent_start` for that plugin while leaving other plugin capabilities enabled.
|
||||
- When disabled, OpenClaw blocks `before_prompt_build` and ignores prompt-mutating fields returned from legacy `before_agent_start` while preserving legacy `modelOverride` and `providerOverride`.
|
||||
|
||||
`before_prompt_build` result fields:
|
||||
|
||||
|
|
|
|||
|
|
@ -767,6 +767,7 @@ describe("config help copy quality", () => {
|
|||
const pluginPromptPolicy = FIELD_HELP["plugins.entries.*.hooks.allowPromptInjection"];
|
||||
expect(pluginPromptPolicy.includes("before_prompt_build")).toBe(true);
|
||||
expect(pluginPromptPolicy.includes("before_agent_start")).toBe(true);
|
||||
expect(pluginPromptPolicy.includes("modelOverride")).toBe(true);
|
||||
});
|
||||
|
||||
it("documents auth/model root semantics and provider secret handling", () => {
|
||||
|
|
|
|||
|
|
@ -914,7 +914,7 @@ export const FIELD_HELP: Record<string, string> = {
|
|||
"plugins.entries.*.hooks":
|
||||
"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.",
|
||||
"plugins.entries.*.hooks.allowPromptInjection":
|
||||
"Controls whether this plugin may register prompt-mutating typed hooks (`before_prompt_build`, `before_agent_start`). Set false to block prompt injection while keeping other plugin capabilities enabled.",
|
||||
"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
|
||||
"plugins.entries.*.apiKey":
|
||||
"Optional API key field consumed by plugins that accept direct key configuration in entry settings. Use secret/env substitution and avoid committing real credentials into config files.",
|
||||
"plugins.entries.*.env":
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export type PluginEntryConfig = {
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
/** Controls typed prompt mutation hooks (before_prompt_build, before_agent_start). */
|
||||
/** Controls prompt mutation via before_prompt_build and prompt fields from legacy before_agent_start. */
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
config?: Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import path from "node:path";
|
|||
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { createHookRunner } from "./hooks.js";
|
||||
import { __testing, loadOpenClawPlugins } from "./loader.js";
|
||||
|
||||
type TempPlugin = { dir: string; file: string; id: string };
|
||||
|
|
@ -685,14 +686,18 @@ describe("loadOpenClawPlugins", () => {
|
|||
expect(disabled?.status).toBe("disabled");
|
||||
});
|
||||
|
||||
it("blocks prompt-injection typed hooks when disabled by plugin hook policy", () => {
|
||||
it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "hook-policy",
|
||||
filename: "hook-policy.cjs",
|
||||
body: `module.exports = { id: "hook-policy", register(api) {
|
||||
api.on("before_prompt_build", () => ({ prependContext: "prepend" }));
|
||||
api.on("before_agent_start", () => ({ prependContext: "legacy" }));
|
||||
api.on("before_agent_start", () => ({
|
||||
prependContext: "legacy",
|
||||
modelOverride: "gpt-4o",
|
||||
providerOverride: "anthropic",
|
||||
}));
|
||||
api.on("before_model_resolve", () => ({ providerOverride: "openai" }));
|
||||
} };`,
|
||||
});
|
||||
|
|
@ -712,13 +717,28 @@ describe("loadOpenClawPlugins", () => {
|
|||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "hook-policy")?.status).toBe("loaded");
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["before_model_resolve"]);
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
|
||||
"before_agent_start",
|
||||
"before_model_resolve",
|
||||
]);
|
||||
const runner = createHookRunner(registry);
|
||||
const legacyResult = await runner.runBeforeAgentStart({ prompt: "hello", messages: [] }, {});
|
||||
expect(legacyResult).toEqual({
|
||||
modelOverride: "gpt-4o",
|
||||
providerOverride: "anthropic",
|
||||
});
|
||||
const blockedDiagnostics = registry.diagnostics.filter((diag) =>
|
||||
String(diag.message).includes(
|
||||
"blocked by plugins.entries.hook-policy.hooks.allowPromptInjection=false",
|
||||
),
|
||||
);
|
||||
expect(blockedDiagnostics).toHaveLength(2);
|
||||
expect(blockedDiagnostics).toHaveLength(1);
|
||||
const constrainedDiagnostics = registry.diagnostics.filter((diag) =>
|
||||
String(diag.message).includes(
|
||||
"prompt fields constrained by plugins.entries.hook-policy.hooks.allowPromptInjection=false",
|
||||
),
|
||||
);
|
||||
expect(constrainedDiagnostics).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps prompt-injection typed hooks enabled by default", () => {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import type {
|
|||
PluginLogger,
|
||||
PluginOrigin,
|
||||
PluginKind,
|
||||
PluginHookBeforeAgentStartResult,
|
||||
PluginHookName,
|
||||
PluginHookHandlerMap,
|
||||
PluginHookRegistration as TypedPluginHookRegistration,
|
||||
|
|
@ -145,6 +146,38 @@ type PluginTypedHookPolicy = {
|
|||
allowPromptInjection?: boolean;
|
||||
};
|
||||
|
||||
const stripPromptMutationFieldsFromLegacyHookResult = (
|
||||
result: PluginHookBeforeAgentStartResult | void,
|
||||
): PluginHookBeforeAgentStartResult | void => {
|
||||
if (!result || typeof result !== "object") {
|
||||
return result;
|
||||
}
|
||||
const {
|
||||
systemPrompt: _systemPrompt,
|
||||
prependContext: _prependContext,
|
||||
prependSystemContext: _prependSystemContext,
|
||||
appendSystemContext: _appendSystemContext,
|
||||
...remaining
|
||||
} = result;
|
||||
return Object.keys(remaining).length > 0
|
||||
? (remaining as PluginHookBeforeAgentStartResult)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
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: [],
|
||||
|
|
@ -496,20 +529,34 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
let effectiveHandler = handler;
|
||||
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(hookName)) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
return;
|
||||
if (hookName === "before_prompt_build") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (hookName === "before_agent_start") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
effectiveHandler = constrainLegacyPromptInjectionHook(
|
||||
handler as PluginHookHandlerMap["before_agent_start"],
|
||||
) as PluginHookHandlerMap[K];
|
||||
}
|
||||
}
|
||||
record.hookCount += 1;
|
||||
registry.typedHooks.push({
|
||||
pluginId: record.id,
|
||||
hookName,
|
||||
handler,
|
||||
handler: effectiveHandler,
|
||||
priority: opts?.priority,
|
||||
source: record.source,
|
||||
} as TypedPluginHookRegistration);
|
||||
|
|
|
|||
Loading…
Reference in New Issue