mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix(plugins): load explicit hook plugins at startup (#76684) (thanks @MkDev11)
Includes explicitly enabled hook-capable plugins in the Gateway startup runtime scope and adds regression coverage for startup hook plugin gating.
This commit is contained in:
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/install: run `npm install` from the managed npm-root manifest so installing one `@openclaw/*` plugin preserves already installed sibling plugins instead of pruning them. Fixes #76571. (#76602) Thanks @byungskers and @crpol.
|
||||
- Plugins/context-engine: include the selected `plugins.slots.contextEngine` plugin in the gateway startup load plan so external context-engine plugins without `activation.onStartup` in their manifest are loaded before any agent turn resolves the active engine; prevents the "Context engine X is not registered; falling back to default engine legacy" warning after gateway startup. Fixes #76576. Thanks @hclsys.
|
||||
- Plugins/tools: restore on-demand registry load for path-based plugins (origin "config") so tool factories registered via `plugins.load.paths` are resolved at agent request time when no pre-warmed channel registry is present; prevents "unknown method" errors after gateway startup. Fixes #76598. Thanks @hclsys.
|
||||
- Plugins/hooks: include explicitly enabled hook-capable plugins in the Gateway startup runtime scope so embedded PI runs can see their `before_prompt_build` and `agent_end` hooks. Fixes #76649. Thanks @wwf3045 and @MkDev11.
|
||||
- Channels/QQ Bot: resolve structured `clientSecret` SecretRefs before QQ token exchange, expose the QQ Bot secret contract to secrets tooling, and reject legacy `secretref:/...` marker strings. (#74772) Thanks @xialonglee.
|
||||
- Agents: keep active streamed provider replies alive by refreshing guarded fetch timeouts on raw body chunks and surface true prompt stream timeouts as explicit errors instead of partial assistant fragments. Fixes #76307. (#76633) Thanks @MkDev11.
|
||||
- Plugins/externalization: keep official ACPX, Google Chat, and LINE install specs on production package names, leaving beta-tag probing to the explicit OpenClaw beta update channel. Thanks @vincentkoc.
|
||||
|
||||
@@ -301,6 +301,25 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
{
|
||||
id: "external-hook-capability",
|
||||
channels: [],
|
||||
activation: {
|
||||
onCapabilities: ["hook"],
|
||||
},
|
||||
origin: "global",
|
||||
enabledByDefault: undefined,
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
{
|
||||
id: "external-hook-policy",
|
||||
channels: [],
|
||||
origin: "global",
|
||||
enabledByDefault: undefined,
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
{
|
||||
id: "lossless-claw",
|
||||
kind: "context-engine",
|
||||
@@ -847,6 +866,173 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads explicit hook-capability plugins at startup", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: createStartupConfig({
|
||||
enabledPluginIds: ["external-hook-capability"],
|
||||
allowPluginIds: ["external-hook-capability"],
|
||||
noConfiguredChannels: true,
|
||||
memorySlot: "none",
|
||||
}),
|
||||
expected: ["external-hook-capability"],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not ambient-load hook-capability plugins at startup", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: createStartupConfig({
|
||||
noConfiguredChannels: true,
|
||||
memorySlot: "none",
|
||||
}),
|
||||
expected: ["browser"],
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks hook-capability plugins when plugins are globally disabled", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
channels: {},
|
||||
plugins: {
|
||||
enabled: false,
|
||||
allow: ["external-hook-capability"],
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
"external-hook-capability": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks hook-capability plugins when explicitly denied", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["external-hook-capability"],
|
||||
deny: ["external-hook-capability"],
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
"external-hook-capability": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("loads explicit hook-policy plugins at startup", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
channels: {},
|
||||
plugins: {
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
"external-hook-policy": {
|
||||
hooks: {
|
||||
allowConversationAccess: true,
|
||||
allowPromptInjection: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: ["external-hook-policy"],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["conversation access", { allowConversationAccess: true }],
|
||||
["prompt injection", { allowPromptInjection: true }],
|
||||
] as const)("loads hook-policy plugins with only %s enabled", (_name, hooks) => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
channels: {},
|
||||
plugins: {
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
"external-hook-policy": {
|
||||
hooks,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: ["external-hook-policy"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps hook-policy plugins behind restrictive allowlists", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["browser"],
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
"external-hook-policy": {
|
||||
hooks: {
|
||||
allowPromptInjection: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let effective-only hook policy bypass the authored startup allowlist", () => {
|
||||
const activationSourceConfig = {
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["browser"],
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const runtimeConfig = {
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["browser", "external-hook-policy"],
|
||||
slots: { memory: "none" },
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
"external-hook-policy": {
|
||||
hooks: {
|
||||
allowPromptInjection: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expectStartupPluginIdsCase({
|
||||
config: runtimeConfig,
|
||||
activationSourceConfig,
|
||||
expected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("starts bundled sidecars selected by root config activation paths", () => {
|
||||
const rawConfig = {
|
||||
browser: {
|
||||
|
||||
@@ -37,6 +37,8 @@ export type GatewayStartupPluginPlan = {
|
||||
pluginIds: readonly string[];
|
||||
};
|
||||
|
||||
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
@@ -282,6 +284,76 @@ function canStartConfiguredRootPlugin(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasExplicitHookPolicyConfig(
|
||||
entry: NormalizedPluginsConfig["entries"][string] | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
entry?.hooks?.allowConversationAccess === true || entry?.hooks?.allowPromptInjection === true
|
||||
);
|
||||
}
|
||||
|
||||
function hasHookRuntimeStartupIntent(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
activationSourcePlugins: NormalizedPluginsConfig;
|
||||
}): boolean {
|
||||
if (params.manifest?.activation?.onCapabilities?.includes("hook")) {
|
||||
return true;
|
||||
}
|
||||
return hasExplicitHookPolicyConfig(
|
||||
params.activationSourcePlugins.entries[params.plugin.pluginId],
|
||||
);
|
||||
}
|
||||
|
||||
function canStartExplicitHookPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
config: OpenClawConfig;
|
||||
pluginsConfig: NormalizedPluginsConfig;
|
||||
activationSource: {
|
||||
plugins: NormalizedPluginsConfig;
|
||||
rootConfig?: OpenClawConfig;
|
||||
};
|
||||
activationSourcePlugins: NormalizedPluginsConfig;
|
||||
}): boolean {
|
||||
const hasHookPolicyIntent = hasExplicitHookPolicyConfig(
|
||||
params.activationSourcePlugins.entries[params.plugin.pluginId],
|
||||
);
|
||||
if (
|
||||
!hasHookRuntimeStartupIntent({
|
||||
plugin: params.plugin,
|
||||
manifest: params.manifest,
|
||||
activationSourcePlugins: params.activationSourcePlugins,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!params.pluginsConfig.enabled || !params.activationSourcePlugins.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
||||
params.activationSourcePlugins.deny.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
||||
params.activationSourcePlugins.entries[params.plugin.pluginId]?.enabled === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: params.plugin.pluginId,
|
||||
origin: params.plugin.origin,
|
||||
config: params.pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: params.plugin.enabledByDefault,
|
||||
activationSource: params.activationSource,
|
||||
});
|
||||
return activationState.enabled && (activationState.explicitlyEnabled || hasHookPolicyIntent);
|
||||
}
|
||||
|
||||
function canStartConfiguredChannelPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
config: OpenClawConfig;
|
||||
@@ -499,6 +571,18 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
canStartExplicitHookPlugin({
|
||||
plugin,
|
||||
manifest,
|
||||
config: params.config,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
activationSourcePlugins,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!shouldConsiderForGatewayStartup({
|
||||
plugin,
|
||||
|
||||
Reference in New Issue
Block a user