fix(plugins): retain cold-loaded tool registries

This commit is contained in:
luoyanglang
2026-05-16 11:45:54 +00:00
committed by Vincent Koc
parent cb13be375d
commit 4141be5c35
4 changed files with 62 additions and 3 deletions
+1
View File
@@ -3118,6 +3118,7 @@ This audited record covers the complete v2026.5.28..v2026.5.31-beta.4 history: 4
- Agents/sandbox: honor explicit Docker sandbox env variables with credential-looking names during container creation, and recreate affected sandbox containers when the effective env policy changes. Fixes #82695. (#82763) Thanks @joshavant.
- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance.
- Plugins: resolve bundled entry, dist-runtime, package-state, and public artifact paths from packaged roots, so bundled plugin probes and hardlinked public surfaces no longer fall back to source files or fail during restart. Fixes #78462. Fixes #75797. Refs #76865. Thanks @ginishuh and @ymebosma.
- Plugins/tools: retain cold-loaded tool registries used by cached descriptors after runtime registry replacement, so contracted plugin tools keep executing in sub-agent sessions instead of failing with `plugin tool runtime missing`. Fixes #80847.
- 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.
@@ -27,7 +27,7 @@ function resolveRuntimeSubagentMode(
return "default";
}
function installStandaloneRegistry(
export function installStandaloneRuntimePluginRegistry(
registry: PluginRegistry,
params: {
loadOptions: PluginLoadOptions;
+45
View File
@@ -2147,6 +2147,51 @@ describe("resolvePluginTools optional tools", () => {
expect(factory).toHaveBeenCalledTimes(2);
});
it("retains cold-loaded plugin tools for cached descriptor execution after active registry replacement", async () => {
const factory = vi.fn(() => makeTool("cached_lifecycle_tool"));
const gatewayRegistry = setRegistry([
{
pluginId: "cache-lifecycle-test",
optional: false,
source: "/tmp/cache-lifecycle-test.js",
names: ["cached_lifecycle_tool"],
factory,
},
]);
const first = resolvePluginTools(
createResolveToolsParams({
toolAllowlist: ["cached_lifecycle_tool"],
allowGatewaySubagentBinding: true,
}),
);
const [tool] = resolvePluginTools(
createResolveToolsParams({
toolAllowlist: ["cached_lifecycle_tool"],
allowGatewaySubagentBinding: true,
}),
);
expectResolvedToolNames(first, ["cached_lifecycle_tool"]);
expect(tool?.name).toBe("cached_lifecycle_tool");
expect(factory).toHaveBeenCalledTimes(1);
const replacementRegistry = createToolRegistry([]);
replacementRegistry.plugins.push({ id: "cache-lifecycle-test", status: "loaded" });
setActivePluginRegistry?.(replacementRegistry as never, "provider-runtime", "default", "/tmp");
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock
.mockReturnValueOnce(gatewayRegistry)
.mockReturnValue(createToolRegistry([]));
await expect(tool?.execute("call-1", {}, undefined)).resolves.toEqual({
content: [{ type: "text", text: "ok" }],
});
await expect(tool?.execute("call-2", {}, undefined)).resolves.toEqual({
content: [{ type: "text", text: "ok" }],
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("does not reuse cached plugin tool descriptors across sandbox context changes", () => {
const factory = vi.fn((rawCtx: unknown) => {
const ctx = rawCtx as { sandboxed?: boolean };
+15 -2
View File
@@ -24,7 +24,10 @@ import {
buildPluginRuntimeLoadOptions,
resolvePluginRuntimeLoadContext,
} from "./runtime/load-context.js";
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
import {
ensureStandaloneRuntimePluginRegistryLoaded,
installStandaloneRuntimePluginRegistry,
} from "./runtime/standalone-runtime-registry-loader.js";
import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
import {
buildPluginToolDescriptorCacheKey,
@@ -925,14 +928,24 @@ function resolvePluginToolRegistry(params: {
}
const forceStandaloneLoad = Boolean(channelRegistry || activeRegistry);
const shouldRetainColdLoadedToolRegistry =
forceStandaloneLoad &&
params.loadOptions.activate === false &&
params.loadOptions.toolDiscovery === true;
const standaloneRegistry = ensureStandaloneRuntimePluginRegistryLoaded({
surface: "active",
forceLoad: forceStandaloneLoad,
installRegistry: !forceStandaloneLoad,
installRegistry: shouldRetainColdLoadedToolRegistry ? false : !forceStandaloneLoad,
requiredPluginIds: params.onlyPluginIds,
loadOptions: params.loadOptions,
});
if (registryHasScopedPluginTools(standaloneRegistry, params.onlyPluginIds)) {
if (shouldRetainColdLoadedToolRegistry) {
installStandaloneRuntimePluginRegistry(standaloneRegistry, {
loadOptions: params.loadOptions,
surface: "active",
});
}
return standaloneRegistry;
}
return standaloneRegistry ?? channelRegistry ?? activeRegistry;