From 8de5a55317c359d1d9efe89178b01fdc09f585de Mon Sep 17 00:00:00 2001 From: VACInc Date: Thu, 7 May 2026 05:40:22 -0400 Subject: [PATCH] Fix Tavily tool SecretRef runtime config Resolve Tavily dedicated tool credential lookup against the active runtime config snapshot. PR: https://github.com/openclaw/openclaw/pull/78610 --- CHANGELOG.md | 1 + extensions/tavily/index.ts | 6 +- extensions/tavily/src/tavily-extract-tool.ts | 18 ++++- extensions/tavily/src/tavily-search-tool.ts | 18 ++++- extensions/tavily/src/tavily-tools.test.ts | 82 ++++++++++++++++++++ 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 982b05d81af..815350bb428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc. - Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero. - fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987. - Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean. diff --git a/extensions/tavily/index.ts b/extensions/tavily/index.ts index cefe792b94c..9fb207261a3 100644 --- a/extensions/tavily/index.ts +++ b/extensions/tavily/index.ts @@ -1,4 +1,4 @@ -import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createTavilyExtractTool } from "./src/tavily-extract-tool.js"; import { createTavilyWebSearchProvider } from "./src/tavily-search-provider.js"; import { createTavilySearchTool } from "./src/tavily-search-tool.js"; @@ -9,7 +9,7 @@ export default definePluginEntry({ description: "Bundled Tavily search and extract plugin", register(api) { api.registerWebSearchProvider(createTavilyWebSearchProvider()); - api.registerTool(createTavilySearchTool(api) as AnyAgentTool); - api.registerTool(createTavilyExtractTool(api) as AnyAgentTool); + api.registerTool((ctx) => createTavilySearchTool(api, ctx), { name: "tavily_search" }); + api.registerTool((ctx) => createTavilyExtractTool(api, ctx), { name: "tavily_extract" }); }, }); diff --git a/extensions/tavily/src/tavily-extract-tool.ts b/extensions/tavily/src/tavily-extract-tool.ts index 98ce7f38f74..11e26b8756c 100644 --- a/extensions/tavily/src/tavily-extract-tool.ts +++ b/extensions/tavily/src/tavily-extract-tool.ts @@ -1,3 +1,5 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; import { jsonResult, @@ -8,6 +10,18 @@ import { Type } from "typebox"; import { runTavilyExtract } from "./tavily-client.js"; import { optionalStringEnum } from "./tavily-tool-schema.js"; +type TavilyToolConfigContext = Pick< + OpenClawPluginToolContext, + "config" | "runtimeConfig" | "getRuntimeConfig" +>; + +function resolveTavilyToolConfig( + api: OpenClawPluginApi, + ctx?: TavilyToolConfigContext, +): OpenClawConfig { + return ctx?.getRuntimeConfig?.() ?? ctx?.runtimeConfig ?? ctx?.config ?? api.config; +} + const TavilyExtractToolSchema = Type.Object( { urls: Type.Array(Type.String(), { @@ -39,7 +53,7 @@ const TavilyExtractToolSchema = Type.Object( { additionalProperties: false }, ); -export function createTavilyExtractTool(api: OpenClawPluginApi) { +export function createTavilyExtractTool(api: OpenClawPluginApi, ctx?: TavilyToolConfigContext) { return { name: "tavily_extract", label: "Tavily Extract", @@ -65,7 +79,7 @@ export function createTavilyExtractTool(api: OpenClawPluginApi) { return jsonResult( await runTavilyExtract({ - cfg: api.config, + cfg: resolveTavilyToolConfig(api, ctx), urls, query, extractDepth, diff --git a/extensions/tavily/src/tavily-search-tool.ts b/extensions/tavily/src/tavily-search-tool.ts index b6b1dbbd8b1..68d03dfc284 100644 --- a/extensions/tavily/src/tavily-search-tool.ts +++ b/extensions/tavily/src/tavily-search-tool.ts @@ -1,3 +1,5 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; import { jsonResult, @@ -8,6 +10,18 @@ import { Type } from "typebox"; import { runTavilySearch } from "./tavily-client.js"; import { optionalStringEnum } from "./tavily-tool-schema.js"; +type TavilyToolConfigContext = Pick< + OpenClawPluginToolContext, + "config" | "runtimeConfig" | "getRuntimeConfig" +>; + +function resolveTavilyToolConfig( + api: OpenClawPluginApi, + ctx?: TavilyToolConfigContext, +): OpenClawConfig { + return ctx?.getRuntimeConfig?.() ?? ctx?.runtimeConfig ?? ctx?.config ?? api.config; +} + const TavilySearchToolSchema = Type.Object( { query: Type.String({ description: "Search query string." }), @@ -46,7 +60,7 @@ const TavilySearchToolSchema = Type.Object( { additionalProperties: false }, ); -export function createTavilySearchTool(api: OpenClawPluginApi) { +export function createTavilySearchTool(api: OpenClawPluginApi, ctx?: TavilyToolConfigContext) { return { name: "tavily_search", label: "Tavily Search", @@ -69,7 +83,7 @@ export function createTavilySearchTool(api: OpenClawPluginApi) { return jsonResult( await runTavilySearch({ - cfg: api.config, + cfg: resolveTavilyToolConfig(api, ctx), query, searchDepth, topic, diff --git a/extensions/tavily/src/tavily-tools.test.ts b/extensions/tavily/src/tavily-tools.test.ts index 7fc4c8d621b..3245c3353e8 100644 --- a/extensions/tavily/src/tavily-tools.test.ts +++ b/extensions/tavily/src/tavily-tools.test.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime"; +import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_TAVILY_BASE_URL, @@ -33,6 +34,7 @@ describe("tavily tools", () => { let createTavilySearchTool: typeof import("./tavily-search-tool.js").createTavilySearchTool; let createTavilyExtractTool: typeof import("./tavily-extract-tool.js").createTavilyExtractTool; let tavilyClientTesting: typeof import("./tavily-client.js").__testing; + let tavilyPlugin: typeof import("../index.js").default; beforeAll(async () => { ({ createTavilyWebSearchProvider } = await import("./tavily-search-provider.js")); @@ -40,6 +42,7 @@ describe("tavily tools", () => { ({ createTavilyExtractTool } = await import("./tavily-extract-tool.js")); ({ __testing: tavilyClientTesting } = await vi.importActual("./tavily-client.js")); + ({ default: tavilyPlugin } = await import("../index.js")); }); beforeEach(() => { @@ -140,6 +143,85 @@ describe("tavily tools", () => { }); }); + it("late-binds dedicated tools to the resolved runtime config snapshot", async () => { + const rawConfig = { + plugins: { + entries: { + tavily: { + config: { + webSearch: { + apiKey: { source: "exec", provider: "default", id: "printf resolved-key" }, + }, + }, + }, + }, + }, + } as OpenClawConfig; + const runtimeConfig = { + plugins: { + entries: { + tavily: { + config: { + webSearch: { + apiKey: "resolved-key", + }, + }, + }, + }, + }, + } as OpenClawConfig; + const registeredTools: Array[0]> = []; + const registeredOptions: Array[1]> = []; + const api = createTestPluginApi({ + config: rawConfig, + registerTool(tool, opts) { + registeredTools.push(tool); + registeredOptions.push(opts); + }, + }); + + tavilyPlugin.register(api); + const searchFactory = registeredTools.find( + (tool, index) => + registeredOptions[index]?.name === "tavily_search" && typeof tool === "function", + ); + const extractFactory = registeredTools.find( + (tool, index) => + registeredOptions[index]?.name === "tavily_extract" && typeof tool === "function", + ); + if (typeof searchFactory !== "function" || typeof extractFactory !== "function") { + throw new Error("Expected Tavily tools to register as runtime-context factories"); + } + + const searchTool = searchFactory({ + config: rawConfig, + runtimeConfig, + }); + const extractTool = extractFactory({ + config: rawConfig, + getRuntimeConfig: () => runtimeConfig, + }); + if (Array.isArray(searchTool) || !searchTool || Array.isArray(extractTool) || !extractTool) { + throw new Error("Expected single Tavily tool definitions"); + } + + await searchTool.execute("search-call", { query: "openclaw" }); + await extractTool.execute("extract-call", { urls: ["https://example.com"] }); + + expect(runTavilySearch).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: runtimeConfig, + query: "openclaw", + }), + ); + expect(runTavilyExtract).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: runtimeConfig, + urls: ["https://example.com"], + }), + ); + }); + it("drops empty domain arrays and forwards query-scoped chunking", async () => { runTavilySearch.mockImplementationOnce(async (params: Record) => ({ ok: true,