fix(plugins): restore cached command registries

This commit is contained in:
Peter Steinberger
2026-04-24 22:49:23 +01:00
parent cabdf5bbc4
commit b0c9810b0f
5 changed files with 114 additions and 2 deletions

View File

@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Discord/gateway: prevent startup from getting stuck at `awaiting gateway readiness` when Carbon gateway registration races with a lifecycle reconnect. Fixes #52372. (#68159) Thanks @IVY-AI-gif.
- Plugins/cache: restore plugin command and interactive handler registries on loader cache hits, so cached external plugins keep slash commands and callback handlers available after reloads. Fixes #71100. Thanks @BomBastikDE.
- Plugin SDK/tool-result transforms: restrict harness tool-result middleware to bundled plugins, fail closed on middleware errors, validate rewritten result shapes, preserve Pi per-call ids, and keep Codex media trust checks anchored to raw tool provenance. Thanks @vincentkoc.
- Gateway/MCP loopback: apply owner-only tool policy and run before-tool-call hooks on `127.0.0.1/mcp` `tools/list` and `tools/call`, so non-owner bearer callers can no longer see or invoke owner-only tools such as `cron`, `gateway`, and `nodes`, matching the existing HTTP `/tools/invoke` and embedded-agent paths. (#71159) Thanks @mmaps.
- Codex harness/security: wait for final app-server approval decisions and sanitize approval preview text, so native Codex permission prompts cannot be resolved by an early placeholder decision or render unsafe terminal/control content. (#70751, #70569) Thanks @Lucenx9.

View File

@@ -51,6 +51,21 @@ export function clearPluginCommandsForPlugin(pluginId: string): void {
}
}
export function listRegisteredPluginCommands(): RegisteredPluginCommand[] {
return Array.from(pluginCommands.values());
}
export function restorePluginCommands(commands: readonly RegisteredPluginCommand[]): void {
pluginCommands.clear();
for (const command of commands) {
const name = normalizeOptionalLowercaseString(command.name);
if (!name) {
continue;
}
pluginCommands.set(`/${name}`, command);
}
}
function resolvePluginNativeName(
command: OpenClawPluginCommandDefinition,
provider?: string,

View File

@@ -70,3 +70,25 @@ export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void
}
}
}
export function listPluginInteractiveHandlers(): RegisteredInteractiveHandler[] {
return Array.from(getPluginInteractiveHandlersState().values());
}
export function restorePluginInteractiveHandlers(
registrations: readonly RegisteredInteractiveHandler[],
): void {
clearPluginInteractiveHandlers();
const interactiveHandlers = getPluginInteractiveHandlersState();
for (const registration of registrations) {
const namespace = normalizePluginInteractiveNamespace(registration.namespace);
if (!namespace) {
continue;
}
interactiveHandlers.set(toPluginInteractiveRegistryKey(registration.channel, namespace), {
...registration,
namespace,
channel: normalizeOptionalLowercaseString(registration.channel) ?? "",
});
}
}

View File

@@ -3178,6 +3178,64 @@ module.exports = { id: "throws-after-import", register() {} };`,
expect(getDetachedTaskLifecycleRuntimeRegistration()?.pluginId).toBe("cached-detached-runtime");
});
it("restores cached command and interactive handler registrations on cache hits", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "cached-command-interactive",
filename: "cached-command-interactive.cjs",
body: `module.exports = {
id: "cached-command-interactive",
register(api) {
api.registerCommand({
name: "hue",
description: "Control Hue lights",
handler: async () => ({ text: "ok" }),
});
api.registerInteractiveHandler({
channel: "telegram",
namespace: "hue",
handle: async () => ({ handled: true }),
});
},
};`,
});
const loadOptions = {
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["cached-command-interactive"],
},
},
onlyPluginIds: ["cached-command-interactive"],
} satisfies Parameters<typeof loadOpenClawPlugins>[0];
loadOpenClawPlugins(loadOptions);
expect(getPluginCommandSpecs()).toEqual([
{ name: "hue", description: "Control Hue lights", acceptsArgs: false },
]);
expect(resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")).toBeDefined();
clearPluginCommands();
clearPluginInteractiveHandlers();
expect(getPluginCommandSpecs()).toEqual([]);
expect(resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")).toBeNull();
loadOpenClawPlugins(loadOptions);
expect(getPluginCommandSpecs()).toEqual([
{ name: "hue", description: "Control Hue lights", acceptsArgs: false },
]);
expect(
resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")?.registration,
).toMatchObject({
pluginId: "cached-command-interactive",
namespace: "hue",
channel: "telegram",
});
});
it("clears stale detached task runtime registrations on active reloads when no plugin re-registers one", () => {
useNoBundledPlugins();
registerDetachedTaskLifecycleRuntime("stale-runtime", createDetachedTaskRuntimeStub("stale"));

View File

@@ -37,7 +37,11 @@ import {
resolveBundledRuntimeDependencyInstallRoot,
type BundledRuntimeDepsInstallParams,
} from "./bundled-runtime-deps.js";
import { clearPluginCommands } from "./command-registry-state.js";
import {
clearPluginCommands,
listRegisteredPluginCommands,
restorePluginCommands,
} from "./command-registry-state.js";
import {
clearCompactionProviders,
listRegisteredCompactionProviders,
@@ -56,7 +60,11 @@ import {
} from "./config-state.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { initializeGlobalHookRunner } from "./hook-runner-global.js";
import { clearPluginInteractiveHandlers } from "./interactive-registry.js";
import {
clearPluginInteractiveHandlers,
listPluginInteractiveHandlers,
restorePluginInteractiveHandlers,
} from "./interactive-registry.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js";
@@ -205,6 +213,8 @@ export class PluginLoadReentryError extends Error {
type CachedPluginState = {
registry: PluginRegistry;
detachedTaskRuntimeRegistration: ReturnType<typeof getDetachedTaskLifecycleRuntimeRegistration>;
commands: ReturnType<typeof listRegisteredPluginCommands>;
interactiveHandlers: ReturnType<typeof listPluginInteractiveHandlers>;
memoryCapability: ReturnType<typeof getMemoryCapabilityRegistration>;
memoryCorpusSupplements: ReturnType<typeof listMemoryCorpusSupplements>;
agentHarnesses: ReturnType<typeof listRegisteredAgentHarnesses>;
@@ -243,8 +253,10 @@ export function clearPluginLoaderCache(): void {
openAllowlistWarningCache.clear();
clearBundledRuntimeDependencyNodePaths();
clearAgentHarnesses();
clearPluginCommands();
clearCompactionProviders();
clearDetachedTaskLifecycleRuntimeRegistration();
clearPluginInteractiveHandlers();
clearMemoryEmbeddingProviders();
clearMemoryPluginState();
}
@@ -1871,8 +1883,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const cached = getCachedPluginRegistry(cacheKey);
if (cached) {
restoreRegisteredAgentHarnesses(cached.agentHarnesses);
restorePluginCommands(cached.commands);
restoreRegisteredCompactionProviders(cached.compactionProviders);
restoreDetachedTaskLifecycleRuntimeRegistration(cached.detachedTaskRuntimeRegistration);
restorePluginInteractiveHandlers(cached.interactiveHandlers);
restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreMemoryPluginState({
capability: cached.memoryCapability,
@@ -2825,7 +2839,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (cacheEnabled) {
setCachedPluginRegistry(cacheKey, {
commands: listRegisteredPluginCommands(),
detachedTaskRuntimeRegistration: getDetachedTaskLifecycleRuntimeRegistration(),
interactiveHandlers: listPluginInteractiveHandlers(),
memoryCapability: getMemoryCapabilityRegistration(),
memoryCorpusSupplements: listMemoryCorpusSupplements(),
registry,