mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
plugins: enforce prompt hook policy with runtime validation (#36567)
Merged via squash.
Prepared head SHA: 6b9d883b6a
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
063e493d3d
commit
688b72e158
@@ -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
|
||||
|
||||
|
||||
@@ -2295,6 +2295,9 @@ See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.5 via LM Studio
|
||||
entries: {
|
||||
"voice-call": {
|
||||
enabled: true,
|
||||
hooks: {
|
||||
allowPromptInjection: false,
|
||||
},
|
||||
config: { provider: "twilio" },
|
||||
},
|
||||
},
|
||||
@@ -2307,6 +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 `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`.
|
||||
|
||||
@@ -486,6 +486,11 @@ Important hooks for prompt construction:
|
||||
- `before_prompt_build`: runs after session load (`messages` are available). Use this to shape prompt input.
|
||||
- `before_agent_start`: legacy compatibility hook. Prefer the two explicit hooks above.
|
||||
|
||||
Core-enforced hook policy:
|
||||
|
||||
- Operators can disable prompt mutation hooks per plugin via `plugins.entries.<id>.hooks.allowPromptInjection: false`.
|
||||
- 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:
|
||||
|
||||
- `prependContext`: prepends text to the user prompt for this run. Best for turn-specific or dynamic content.
|
||||
|
||||
@@ -48,6 +48,38 @@ describe("ui.seamColor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugins.entries.*.hooks.allowPromptInjection", () => {
|
||||
it("accepts boolean values", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
hooks: {
|
||||
allowPromptInjection: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non-boolean values", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
hooks: {
|
||||
allowPromptInjection: "no",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("web search provider config", () => {
|
||||
it("accepts kimi provider and config", () => {
|
||||
const res = validateConfigObject(
|
||||
|
||||
@@ -339,6 +339,8 @@ const TARGET_KEYS = [
|
||||
"plugins.slots",
|
||||
"plugins.entries",
|
||||
"plugins.entries.*.enabled",
|
||||
"plugins.entries.*.hooks",
|
||||
"plugins.entries.*.hooks.allowPromptInjection",
|
||||
"plugins.entries.*.apiKey",
|
||||
"plugins.entries.*.env",
|
||||
"plugins.entries.*.config",
|
||||
@@ -761,6 +763,11 @@ describe("config help copy quality", () => {
|
||||
|
||||
const pluginEnv = FIELD_HELP["plugins.entries.*.env"];
|
||||
expect(/scope|plugin|environment/i.test(pluginEnv)).toBe(true);
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -911,6 +911,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Per-plugin settings keyed by plugin ID including enablement and plugin-specific runtime configuration payloads. Use this for scoped plugin tuning without changing global loader policy.",
|
||||
"plugins.entries.*.enabled":
|
||||
"Per-plugin enablement override for a specific entry, applied on top of global plugin policy (restart required). Use this to stage plugin rollout gradually across environments.",
|
||||
"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 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":
|
||||
|
||||
@@ -797,6 +797,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"plugins.slots.memory": "Memory Plugin",
|
||||
"plugins.entries": "Plugin Entries",
|
||||
"plugins.entries.*.enabled": "Plugin Enabled",
|
||||
"plugins.entries.*.hooks": "Plugin Hook Policy",
|
||||
"plugins.entries.*.hooks.allowPromptInjection": "Allow Prompt Injection Hooks",
|
||||
"plugins.entries.*.apiKey": "Plugin API Key",
|
||||
"plugins.entries.*.env": "Plugin Environment Variables",
|
||||
"plugins.entries.*.config": "Plugin Config",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export type PluginEntryConfig = {
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
/** Controls prompt mutation via before_prompt_build and prompt fields from legacy before_agent_start. */
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
|
||||
@@ -149,6 +149,12 @@ const SkillEntrySchema = z
|
||||
const PluginEntrySchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
hooks: z
|
||||
.object({
|
||||
allowPromptInjection: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -47,6 +47,32 @@ describe("normalizePluginsConfig", () => {
|
||||
});
|
||||
expect(result.slots.memory).toBe("memory-core");
|
||||
});
|
||||
|
||||
it("normalizes plugin hook policy flags", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
entries: {
|
||||
"voice-call": {
|
||||
hooks: {
|
||||
allowPromptInjection: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.hooks?.allowPromptInjection).toBe(false);
|
||||
});
|
||||
|
||||
it("drops invalid plugin hook policy values", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
entries: {
|
||||
"voice-call": {
|
||||
hooks: {
|
||||
allowPromptInjection: "nope",
|
||||
} as unknown as { allowPromptInjection: boolean },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.entries["voice-call"]?.hooks).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEffectiveEnableState", () => {
|
||||
|
||||
@@ -11,7 +11,16 @@ export type NormalizedPluginsConfig = {
|
||||
slots: {
|
||||
memory?: string | null;
|
||||
};
|
||||
entries: Record<string, { enabled?: boolean; config?: unknown }>;
|
||||
entries: Record<
|
||||
string,
|
||||
{
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export const BUNDLED_ENABLED_BY_DEFAULT = new Set<string>([
|
||||
@@ -55,8 +64,23 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr
|
||||
continue;
|
||||
}
|
||||
const entry = value as Record<string, unknown>;
|
||||
const hooksRaw = entry.hooks;
|
||||
const hooks =
|
||||
hooksRaw && typeof hooksRaw === "object" && !Array.isArray(hooksRaw)
|
||||
? {
|
||||
allowPromptInjection: (hooksRaw as { allowPromptInjection?: unknown })
|
||||
.allowPromptInjection,
|
||||
}
|
||||
: undefined;
|
||||
const normalizedHooks =
|
||||
hooks && typeof hooks.allowPromptInjection === "boolean"
|
||||
? {
|
||||
allowPromptInjection: hooks.allowPromptInjection,
|
||||
}
|
||||
: undefined;
|
||||
normalized[key] = {
|
||||
enabled: typeof entry.enabled === "boolean" ? entry.enabled : undefined,
|
||||
hooks: normalizedHooks,
|
||||
config: "config" in entry ? entry.config : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,6 +686,122 @@ describe("loadOpenClawPlugins", () => {
|
||||
expect(disabled?.status).toBe("disabled");
|
||||
});
|
||||
|
||||
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",
|
||||
modelOverride: "gpt-4o",
|
||||
providerOverride: "anthropic",
|
||||
}));
|
||||
api.on("before_model_resolve", () => ({ providerOverride: "openai" }));
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["hook-policy"],
|
||||
entries: {
|
||||
"hook-policy": {
|
||||
hooks: {
|
||||
allowPromptInjection: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "hook-policy")?.status).toBe("loaded");
|
||||
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(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", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "hook-policy-default",
|
||||
filename: "hook-policy-default.cjs",
|
||||
body: `module.exports = { id: "hook-policy-default", register(api) {
|
||||
api.on("before_prompt_build", () => ({ prependContext: "prepend" }));
|
||||
api.on("before_agent_start", () => ({ prependContext: "legacy" }));
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["hook-policy-default"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
|
||||
"before_prompt_build",
|
||||
"before_agent_start",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores unknown typed hooks from plugins and keeps loading", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "hook-unknown",
|
||||
filename: "hook-unknown.cjs",
|
||||
body: `module.exports = { id: "hook-unknown", register(api) {
|
||||
api.on("totally_unknown_hook_name", () => ({ foo: "bar" }));
|
||||
api.on(123, () => ({ foo: "baz" }));
|
||||
api.on("before_model_resolve", () => ({ providerOverride: "openai" }));
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["hook-unknown"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "hook-unknown")?.status).toBe("loaded");
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["before_model_resolve"]);
|
||||
const unknownHookDiagnostics = registry.diagnostics.filter((diag) =>
|
||||
String(diag.message).includes('unknown typed hook "'),
|
||||
);
|
||||
expect(unknownHookDiagnostics).toHaveLength(2);
|
||||
expect(
|
||||
unknownHookDiagnostics.some((diag) =>
|
||||
String(diag.message).includes('unknown typed hook "totally_unknown_hook_name" ignored'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
unknownHookDiagnostics.some((diag) =>
|
||||
String(diag.message).includes('unknown typed hook "123" ignored'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("enforces memory slot selection", () => {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
const memoryA = writePlugin({
|
||||
|
||||
@@ -796,6 +796,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const api = createApi(record, {
|
||||
config: cfg,
|
||||
pluginConfig: validatedConfig.value,
|
||||
hookPolicy: entry?.hooks,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,11 @@ import { resolveUserPath } from "../utils.js";
|
||||
import { registerPluginCommand } from "./commands.js";
|
||||
import { normalizePluginHttpPath } from "./http-path.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import {
|
||||
isPluginHookName,
|
||||
isPromptInjectionHookName,
|
||||
stripPromptMutationFieldsFromLegacyHookResult,
|
||||
} from "./types.js";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginChannelRegistration,
|
||||
@@ -140,6 +145,24 @@ export type PluginRegistryParams = {
|
||||
runtime: PluginRuntime;
|
||||
};
|
||||
|
||||
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: [],
|
||||
@@ -480,12 +503,45 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
hookName: K,
|
||||
handler: PluginHookHandlerMap[K],
|
||||
opts?: { priority?: number },
|
||||
policy?: PluginTypedHookPolicy,
|
||||
) => {
|
||||
if (!isPluginHookName(hookName)) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `unknown typed hook "${String(hookName)}" ignored`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let effectiveHandler = handler;
|
||||
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(hookName)) {
|
||||
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);
|
||||
@@ -503,6 +559,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
params: {
|
||||
config: OpenClawPluginApi["config"];
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
hookPolicy?: PluginTypedHookPolicy;
|
||||
},
|
||||
): OpenClawPluginApi => {
|
||||
return {
|
||||
@@ -526,7 +583,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registerService: (service) => registerService(record, service),
|
||||
registerCommand: (command) => registerCommand(record, command),
|
||||
resolvePath: (input: string) => resolveUserPath(input),
|
||||
on: (hookName, handler, opts) => registerTypedHook(record, hookName, handler, opts),
|
||||
on: (hookName, handler, opts) =>
|
||||
registerTypedHook(record, hookName, handler, opts, params.hookPolicy),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -333,6 +333,55 @@ export type PluginHookName =
|
||||
| "gateway_start"
|
||||
| "gateway_stop";
|
||||
|
||||
export const PLUGIN_HOOK_NAMES = [
|
||||
"before_model_resolve",
|
||||
"before_prompt_build",
|
||||
"before_agent_start",
|
||||
"llm_input",
|
||||
"llm_output",
|
||||
"agent_end",
|
||||
"before_compaction",
|
||||
"after_compaction",
|
||||
"before_reset",
|
||||
"message_received",
|
||||
"message_sending",
|
||||
"message_sent",
|
||||
"before_tool_call",
|
||||
"after_tool_call",
|
||||
"tool_result_persist",
|
||||
"before_message_write",
|
||||
"session_start",
|
||||
"session_end",
|
||||
"subagent_spawning",
|
||||
"subagent_delivery_target",
|
||||
"subagent_spawned",
|
||||
"subagent_ended",
|
||||
"gateway_start",
|
||||
"gateway_stop",
|
||||
] as const satisfies readonly PluginHookName[];
|
||||
|
||||
type MissingPluginHookNames = Exclude<PluginHookName, (typeof PLUGIN_HOOK_NAMES)[number]>;
|
||||
type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? true : never;
|
||||
const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true;
|
||||
void assertAllPluginHookNamesListed;
|
||||
|
||||
const pluginHookNameSet = new Set<PluginHookName>(PLUGIN_HOOK_NAMES);
|
||||
|
||||
export const isPluginHookName = (hookName: unknown): hookName is PluginHookName =>
|
||||
typeof hookName === "string" && pluginHookNameSet.has(hookName as PluginHookName);
|
||||
|
||||
export const PROMPT_INJECTION_HOOK_NAMES = [
|
||||
"before_prompt_build",
|
||||
"before_agent_start",
|
||||
] as const satisfies readonly PluginHookName[];
|
||||
|
||||
export type PromptInjectionHookName = (typeof PROMPT_INJECTION_HOOK_NAMES)[number];
|
||||
|
||||
const promptInjectionHookNameSet = new Set<PluginHookName>(PROMPT_INJECTION_HOOK_NAMES);
|
||||
|
||||
export const isPromptInjectionHookName = (hookName: PluginHookName): boolean =>
|
||||
promptInjectionHookNameSet.has(hookName);
|
||||
|
||||
// Agent context shared across agent hooks
|
||||
export type PluginHookAgentContext = {
|
||||
agentId?: string;
|
||||
@@ -381,6 +430,22 @@ export type PluginHookBeforePromptBuildResult = {
|
||||
appendSystemContext?: string;
|
||||
};
|
||||
|
||||
export const PLUGIN_PROMPT_MUTATION_RESULT_FIELDS = [
|
||||
"systemPrompt",
|
||||
"prependContext",
|
||||
"prependSystemContext",
|
||||
"appendSystemContext",
|
||||
] as const satisfies readonly (keyof PluginHookBeforePromptBuildResult)[];
|
||||
|
||||
type MissingPluginPromptMutationResultFields = Exclude<
|
||||
keyof PluginHookBeforePromptBuildResult,
|
||||
(typeof PLUGIN_PROMPT_MUTATION_RESULT_FIELDS)[number]
|
||||
>;
|
||||
type AssertAllPluginPromptMutationResultFieldsListed =
|
||||
MissingPluginPromptMutationResultFields extends never ? true : never;
|
||||
const assertAllPluginPromptMutationResultFieldsListed: AssertAllPluginPromptMutationResultFieldsListed = true;
|
||||
void assertAllPluginPromptMutationResultFieldsListed;
|
||||
|
||||
// before_agent_start hook (legacy compatibility: combines both phases)
|
||||
export type PluginHookBeforeAgentStartEvent = {
|
||||
prompt: string;
|
||||
@@ -391,6 +456,26 @@ export type PluginHookBeforeAgentStartEvent = {
|
||||
export type PluginHookBeforeAgentStartResult = PluginHookBeforePromptBuildResult &
|
||||
PluginHookBeforeModelResolveResult;
|
||||
|
||||
export type PluginHookBeforeAgentStartOverrideResult = Omit<
|
||||
PluginHookBeforeAgentStartResult,
|
||||
keyof PluginHookBeforePromptBuildResult
|
||||
>;
|
||||
|
||||
export const stripPromptMutationFieldsFromLegacyHookResult = (
|
||||
result: PluginHookBeforeAgentStartResult | void,
|
||||
): PluginHookBeforeAgentStartOverrideResult | void => {
|
||||
if (!result || typeof result !== "object") {
|
||||
return result;
|
||||
}
|
||||
const remaining: Partial<PluginHookBeforeAgentStartResult> = { ...result };
|
||||
for (const field of PLUGIN_PROMPT_MUTATION_RESULT_FIELDS) {
|
||||
delete remaining[field];
|
||||
}
|
||||
return Object.keys(remaining).length > 0
|
||||
? (remaining as PluginHookBeforeAgentStartOverrideResult)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// llm_input hook
|
||||
export type PluginHookLlmInputEvent = {
|
||||
runId: string;
|
||||
|
||||
Reference in New Issue
Block a user