mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 20:44:47 +00:00
fix(plugins): accept deactivate hook alias
This commit is contained in:
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc.
|
||||
- Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn.
|
||||
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant.
|
||||
- Plugins: accept `api.on("deactivate")` as a compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown instead of being ignored as unknown hooks.
|
||||
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media or keep image file extensions when staged.
|
||||
- Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs.
|
||||
- Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments.
|
||||
|
||||
@@ -145,6 +145,7 @@ observation-only.
|
||||
**Lifecycle**
|
||||
|
||||
- `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway
|
||||
- `deactivate` - compatibility alias for `gateway_stop`; prefer `gateway_stop` in new plugins
|
||||
- `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled)
|
||||
- **`before_install`** - inspect skill or plugin install scans and optionally block
|
||||
|
||||
@@ -437,6 +438,8 @@ before the next major release:
|
||||
- **`before_agent_start`** remains for compatibility. New plugins should use
|
||||
`before_model_resolve` and `before_prompt_build` instead of the combined
|
||||
phase.
|
||||
- **`deactivate`** remains as a cleanup compatibility alias. New plugins should
|
||||
use `gateway_stop`.
|
||||
- **`onResolution` in `before_tool_call`** now uses the typed
|
||||
`PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` /
|
||||
`timeout` / `cancelled`) instead of a free-form `string`.
|
||||
|
||||
@@ -94,6 +94,7 @@ export type PluginHookName =
|
||||
| "subagent_delivery_target"
|
||||
| "subagent_spawned"
|
||||
| "subagent_ended"
|
||||
| "deactivate"
|
||||
| "gateway_start"
|
||||
| "gateway_stop"
|
||||
| "heartbeat_prompt_contribution"
|
||||
@@ -132,6 +133,7 @@ export const PLUGIN_HOOK_NAMES = [
|
||||
"subagent_delivery_target",
|
||||
"subagent_spawned",
|
||||
"subagent_ended",
|
||||
"deactivate",
|
||||
"gateway_start",
|
||||
"gateway_stop",
|
||||
"heartbeat_prompt_contribution",
|
||||
@@ -1000,6 +1002,17 @@ export type PluginHookHandlerMap = {
|
||||
event: PluginHookSubagentEndedEvent,
|
||||
ctx: PluginHookSubagentContext,
|
||||
) => Promise<void> | void;
|
||||
/**
|
||||
* Compatibility alias for gateway_stop.
|
||||
*
|
||||
* New plugins should register gateway_stop directly; the loader normalizes
|
||||
* deactivate registrations onto gateway_stop so cleanup handlers still run
|
||||
* during Gateway shutdown.
|
||||
*/
|
||||
deactivate: (
|
||||
event: PluginHookGatewayStopEvent,
|
||||
ctx: PluginHookGatewayContext,
|
||||
) => Promise<void> | void;
|
||||
gateway_start: (
|
||||
event: PluginHookGatewayStartEvent,
|
||||
ctx: PluginHookGatewayContext,
|
||||
|
||||
@@ -5906,6 +5906,44 @@ module.exports = {
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes legacy deactivate typed hooks onto gateway_stop", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "legacy-deactivate-hook",
|
||||
filename: "legacy-deactivate-hook.cjs",
|
||||
body: `module.exports = { id: "legacy-deactivate-hook", register(api) {
|
||||
api.on("deactivate", () => undefined);
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["legacy-deactivate-hook"],
|
||||
entries: {
|
||||
"legacy-deactivate-hook": {
|
||||
hooks: {
|
||||
timeoutMs: 250,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "legacy-deactivate-hook")?.status).toBe(
|
||||
"loaded",
|
||||
);
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["gateway_stop"]);
|
||||
expect(registry.typedHooks[0]?.timeoutMs).toBe(250);
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.pluginId === "legacy-deactivate-hook" &&
|
||||
diag.message === 'typed hook "deactivate" is a compatibility alias for "gateway_stop"',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores unknown typed hooks from plugins and keeps loading", () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
|
||||
@@ -2298,14 +2298,23 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const effectiveHookName = hookName === "deactivate" ? "gateway_stop" : hookName;
|
||||
if (hookName === "deactivate") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: 'typed hook "deactivate" is a compatibility alias for "gateway_stop"',
|
||||
});
|
||||
}
|
||||
let effectiveHandler = handler;
|
||||
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(hookName)) {
|
||||
if (hookName !== "before_agent_start") {
|
||||
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(effectiveHookName)) {
|
||||
if (effectiveHookName !== "before_agent_start") {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
message: `typed hook "${effectiveHookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2313,13 +2322,13 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
message: `typed hook "${effectiveHookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
|
||||
});
|
||||
effectiveHandler = constrainLegacyPromptInjectionHook(
|
||||
handler as PluginHookHandlerMap["before_agent_start"],
|
||||
) as PluginHookHandlerMap[K];
|
||||
}
|
||||
if (isConversationHookName(hookName)) {
|
||||
if (isConversationHookName(effectiveHookName)) {
|
||||
const explicitConversationAccess = policy?.allowConversationAccess;
|
||||
if (record.origin !== "bundled" && explicitConversationAccess !== true) {
|
||||
pushDiagnostic({
|
||||
@@ -2327,7 +2336,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message:
|
||||
`typed hook "${hookName}" blocked because non-bundled plugins must set ` +
|
||||
`typed hook "${effectiveHookName}" blocked because non-bundled plugins must set ` +
|
||||
`plugins.entries.${record.id}.hooks.allowConversationAccess=true`,
|
||||
});
|
||||
return;
|
||||
@@ -2337,16 +2346,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowConversationAccess=false`,
|
||||
message: `typed hook "${effectiveHookName}" blocked by plugins.entries.${record.id}.hooks.allowConversationAccess=false`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const timeoutMs = resolveTypedHookTimeoutMs({ hookName, opts, policy });
|
||||
const timeoutMs = resolveTypedHookTimeoutMs({ hookName: effectiveHookName, opts, policy });
|
||||
record.hookCount += 1;
|
||||
registry.typedHooks.push({
|
||||
pluginId: record.id,
|
||||
hookName,
|
||||
hookName: effectiveHookName,
|
||||
handler: effectiveHandler,
|
||||
priority: opts?.priority,
|
||||
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
||||
|
||||
Reference in New Issue
Block a user