diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 565d0578108..fcb66c1361e 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -277,7 +277,19 @@ function installToolManifestSnapshots(params: { policyHash: "test", generatedAtMs: 0, installRecords: {}, - plugins: [], + plugins: plugins.map((plugin) => ({ + pluginId: String(plugin.id), + origin: plugin.origin, + enabled: true, + enabledByDefault: plugin.enabledByDefault, + startup: { + sidecar: false, + memory: false, + deferConfiguredChannelFullLoadUntilAfterListen: false, + agentHarnesses: [], + }, + compat: [], + })), diagnostics: [], }, registryDiagnostics: [], @@ -301,7 +313,7 @@ function installToolManifestSnapshots(params: { manifestRegistryMs: 0, ownerMapsMs: 0, totalMs: 0, - indexPluginCount: 0, + indexPluginCount: plugins.length, manifestPluginCount: plugins.length, }, } as never, @@ -491,16 +503,28 @@ describe("resolvePluginTools optional tools", () => { ); }); - it("auto-loads cold registry for path-based (bundled-origin) plugins without pre-warming (#76598)", () => { - const config = createContext().config; + it("auto-loads cold registry for path-based config-origin plugins without pre-warming (#76598)", () => { + const context = { + ...createContext(), + config: { + ...createContext().config, + plugins: { + ...createContext().config.plugins, + entries: { + "optional-demo": { enabled: true }, + }, + }, + }, + }; + const config = context.config; const registry = createToolRegistry([createOptionalDemoEntry()]); loadOpenClawPluginsMock.mockReturnValue(registry); installToolManifestSnapshot({ config, plugin: { id: "optional-demo", - origin: "bundled", - enabledByDefault: true, + origin: "config", + enabledByDefault: undefined, channels: [], providers: [], contracts: { @@ -514,6 +538,7 @@ describe("resolvePluginTools optional tools", () => { // This is the regression path from PR #76004 where path-based plugin tools disappeared. const tools = resolvePluginTools( createResolveToolsParams({ + context, toolAllowlist: ["optional_tool"], }), ); @@ -528,6 +553,50 @@ describe("resolvePluginTools optional tools", () => { ); }); + it("warns when cold registry load still does not provide the selected plugin tools", () => { + const context = { + ...createContext(), + config: { + ...createContext().config, + plugins: { + ...createContext().config.plugins, + entries: { + "optional-demo": { enabled: true }, + }, + }, + }, + }; + const config = context.config; + const registry = createToolRegistry([]); + loadOpenClawPluginsMock.mockReturnValue(registry); + installToolManifestSnapshot({ + config, + plugin: { + id: "optional-demo", + origin: "config", + enabledByDefault: undefined, + channels: [], + providers: [], + contracts: { + tools: ["optional_tool"], + }, + }, + }); + + const tools = resolvePluginTools( + createResolveToolsParams({ + context, + toolAllowlist: ["optional_tool"], + }), + ); + + expect(tools).toEqual([]); + expectSingleDiagnosticMessage( + registry.diagnostics, + "plugin tool registry did not include selected plugin tools after cold load (optional-demo)", + ); + }); + it("does not reuse a pinned gateway registry for manifest-unavailable tools", () => { const config = createContext().config; installToolManifestSnapshot({ diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 50b56913997..adddbad4f06 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -812,21 +812,47 @@ export function resolvePluginTools(params: { // Cold registry: path-based plugins (origin "config") registered via plugins.load.paths // are not pinned to any active channel/surface registry until explicitly loaded. // Trigger a standalone load so their tool factories become available, then retry. - ensureStandaloneRuntimePluginRegistryLoaded({ - surface: "channel", - requiredPluginIds: runtimePluginIds, - loadOptions, - }); + try { + ensureStandaloneRuntimePluginRegistryLoaded({ + surface: "channel", + requiredPluginIds: runtimePluginIds, + loadOptions, + }); + } catch (error) { + context.logger.error( + `failed to cold-load plugin tool registry for plugin ids [${runtimePluginIds.join(", ")}]: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + throw error; + } registry = resolvePluginToolRegistry({ loadOptions, onlyPluginIds: runtimePluginIds, }); if (!registry) { + context.logger.warn( + `plugin tool registry still unavailable after cold load for plugin ids [${runtimePluginIds.join( + ", ", + )}]`, + ); return tools; } } const scopedPluginIds = new Set(runtimePluginIds); + const registryToolPluginIds = new Set(registry.tools.map((entry) => entry.pluginId)); + const missingRegistryToolPluginIds = runtimePluginIds.filter( + (pluginId) => !registryToolPluginIds.has(pluginId), + ); + for (const pluginId of missingRegistryToolPluginIds) { + registry.diagnostics.push({ + level: "warn", + pluginId, + source: "plugin-tools", + message: `plugin tool registry did not include selected plugin tools after cold load (${pluginId})`, + }); + } const blockedPlugins = new Set(); const factoryTimingStartedAt = Date.now(); const factoryTimings: PluginToolFactoryTiming[] = [];