fix(gateway): keep effective tools on hot registry path

This commit is contained in:
Peter Steinberger
2026-04-27 11:50:55 +01:00
parent 9dcd53c0b6
commit 6ae2e9e9dc
5 changed files with 63 additions and 25 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- Memory-core: re-resolve the active runtime config whenever `memory_search` or `memory_get` executes, so provider changes made by `config.patch` stop leaving stale embedding backends behind in existing tool instances. Fixes #61098. Thanks @BradGroux and @Linux2010.
- WebChat: keep bare `/new` and `/reset` startup instructions out of visible chat history while preserving `/reset <note>` as user-visible transcript text. Fixes #72369. Thanks @collynes and @haishmg.
- CLI/doctor: remove dangling channel config, heartbeat targets, and channel model overrides when stale plugin repair removes a missing channel plugin, preventing Gateway boot loops after failed plugin reinstalls. Fixes #65293. Thanks @yidecode.
- Control UI/Gateway: reuse the gateway-bound plugin registry and avoid model/auth discovery while resolving effective tool inventory, so chat runs no longer stall Control UI requests on repeated plugin/model setup. Fixes #72365; supersedes #72558. Thanks @Gabiii2398 and @1yihui.
- Channels/setup: treat bundled channel plugins as already bundled during `channels add` and onboarding, enabling them without writing redundant `plugins.load.paths` entries or path install records. Fixes #72740. Thanks @iCodePoet.
- WhatsApp: honor gateway `HTTPS_PROXY` / `HTTP_PROXY` env vars for QR-login WebSocket connections, while respecting `NO_PROXY`, so proxied networks no longer fall back to direct `mmg.whatsapp.net` connections that time out with 408. Fixes #72547; supersedes #72692. Thanks @mebusw and @SymbolStar.
- Bonjour: default mDNS advertisements to the system hostname when it is DNS-safe, avoiding `openclaw.local` probing conflicts and Gateway restart loops on hosts such as `Lobster` or `ubuntu`. Fixes #72355 and #72689; supersedes #72694. Thanks @mscheuerlein-bot, @gcusms, @moyuwuhen601, @pavan987, @zml-0912, @hhq365, and @SymbolStar.

View File

@@ -54,6 +54,7 @@ export function resolveOpenClawPluginToolsForOptions(params: {
}),
existingToolNames: params.existingToolNames ?? new Set<string>(),
toolAllowlist: params.options?.pluginToolAllowlist,
allowGatewaySubagentBinding: params.options?.allowGatewaySubagentBinding,
});
return applyPluginToolDeliveryDefaults({

View File

@@ -120,6 +120,26 @@ describe("createOpenClawTools browser plugin integration", () => {
expect(details.workspaceOnly).toBe(true);
});
it("forwards gateway subagent binding to plugin resolution", () => {
hoisted.resolvePluginTools.mockReturnValue([]);
const config = {
plugins: {
allow: ["browser"],
},
} as OpenClawConfig;
resolveOpenClawPluginToolsForOptions({
options: { config, allowGatewaySubagentBinding: true },
resolvedConfig: config,
});
expect(hoisted.resolvePluginTools).toHaveBeenCalledWith(
expect.objectContaining({
allowGatewaySubagentBinding: true,
}),
);
});
it("does not pass a stale active snapshot as plugin runtime config for a resolved run config", () => {
const staleSourceConfig = {
plugins: {

View File

@@ -23,7 +23,6 @@ const effectiveInventoryState = vi.hoisted(() => ({
pluginMeta: {} as Record<string, { pluginId: string } | undefined>,
channelMeta: {} as Record<string, { channelId: string } | undefined>,
effectivePolicy: {} as { profile?: string; providerProfile?: string },
resolvedModelCompat: undefined as Record<string, unknown> | undefined,
createToolsMock: vi.fn<typeof createOpenClawCodingTools>(
(_options) =>
[
@@ -48,16 +47,6 @@ vi.mock("./pi-tools.js", () => ({
effectiveInventoryState.createToolsMock(options),
}));
vi.mock("./pi-embedded-runner/model.js", () => ({
resolveModel: vi.fn(() => ({
model: effectiveInventoryState.resolvedModelCompat
? { compat: effectiveInventoryState.resolvedModelCompat }
: undefined,
authStorage: {} as never,
modelRegistry: {} as never,
})),
}));
vi.mock("../plugins/tools.js", () => ({
getPluginToolMeta: (tool: { name: string }) => effectiveInventoryState.pluginMeta[tool.name],
}));
@@ -79,7 +68,6 @@ async function loadHarness(options?: {
pluginMeta?: Record<string, { pluginId: string } | undefined>;
channelMeta?: Record<string, { channelId: string } | undefined>;
effectivePolicy?: { profile?: string; providerProfile?: string };
resolvedModelCompat?: Record<string, unknown>;
}) {
effectiveInventoryState.tools = options?.tools ?? [
mockTool({ name: "exec", label: "Exec", description: "Run shell commands" }),
@@ -88,7 +76,6 @@ async function loadHarness(options?: {
effectiveInventoryState.pluginMeta = options?.pluginMeta ?? {};
effectiveInventoryState.channelMeta = options?.channelMeta ?? {};
effectiveInventoryState.effectivePolicy = options?.effectivePolicy ?? {};
effectiveInventoryState.resolvedModelCompat = options?.resolvedModelCompat;
effectiveInventoryState.createToolsMock =
options?.createToolsMock ??
vi.fn<typeof createOpenClawCodingTools>((_options) => effectiveInventoryState.tools);
@@ -111,7 +98,6 @@ describe("resolveEffectiveToolInventory", () => {
effectiveInventoryState.pluginMeta = {};
effectiveInventoryState.channelMeta = {};
effectiveInventoryState.effectivePolicy = {};
effectiveInventoryState.resolvedModelCompat = undefined;
effectiveInventoryState.createToolsMock = vi.fn<typeof createOpenClawCodingTools>(
(_options) => effectiveInventoryState.tools,
);
@@ -312,11 +298,31 @@ describe("resolveEffectiveToolInventory", () => {
]);
const { resolveEffectiveToolInventory } = await loadHarness({
createToolsMock,
resolvedModelCompat: { supportsTools: true, supportsNativeWebSearch: true },
});
resolveEffectiveToolInventory({
cfg: {},
cfg: {
models: {
providers: {
xai: {
baseUrl: "https://api.x.ai/v1",
models: [
{
id: "grok-test",
name: "Grok Test",
api: "openai-completions",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
compat: { supportsTools: true, nativeWebSearchTool: true },
},
],
},
},
},
},
agentDir: "/tmp/agents/main/agent",
modelProvider: "xai",
modelId: "grok-test",
@@ -325,7 +331,7 @@ describe("resolveEffectiveToolInventory", () => {
expect(createToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
allowGatewaySubagentBinding: true,
modelCompat: { supportsTools: true, supportsNativeWebSearch: true },
modelCompat: { supportsTools: true, nativeWebSearchTool: true },
}),
);
});

View File

@@ -7,9 +7,10 @@ import {
} from "../shared/string-coerce.js";
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
import { getChannelAgentToolMeta } from "./channel-tools.js";
import { resolveModel } from "./pi-embedded-runner/model.js";
import { normalizeStaticProviderModelId } from "./model-ref-shared.js";
import { createOpenClawCodingTools } from "./pi-tools.js";
import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js";
import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js";
import { summarizeToolDescriptionText } from "./tool-description-summary.js";
import { resolveToolDisplay } from "./tool-display.js";
import { normalizeToolName } from "./tool-policy.js";
@@ -164,20 +165,30 @@ function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveTo
function resolveEffectiveModelCompat(params: {
cfg: OpenClawConfig;
agentDir: string;
modelProvider?: string;
modelId?: string;
}) {
const provider = params.modelProvider?.trim();
const modelId = params.modelId?.trim();
const provider = normalizeProviderId(params.modelProvider ?? "");
const modelId = params.modelId?.trim() ?? "";
if (!provider || !modelId) {
return undefined;
}
try {
return extractModelCompat(resolveModel(provider, modelId, params.agentDir, params.cfg).model);
} catch {
const providerConfig = findNormalizedProviderValue(params.cfg.models?.providers, provider);
const models = Array.isArray(providerConfig?.models) ? providerConfig.models : [];
if (models.length === 0) {
return undefined;
}
const normalizedModelId = normalizeStaticProviderModelId(provider, modelId);
const normalizedModelKey = normalizeLowercaseStringOrEmpty(normalizedModelId);
const providerPrefixedModelKey = normalizeLowercaseStringOrEmpty(
`${provider}/${normalizedModelId}`,
);
const match = models.find((model) => {
const id = normalizeStaticProviderModelId(provider, model.id);
const key = normalizeLowercaseStringOrEmpty(id);
return key === normalizedModelKey || key === providerPrefixedModelKey;
});
return extractModelCompat(match);
}
export function resolveEffectiveToolInventory(
@@ -190,7 +201,6 @@ export function resolveEffectiveToolInventory(
const agentDir = params.agentDir ?? resolveAgentDir(params.cfg, agentId);
const modelCompat = resolveEffectiveModelCompat({
cfg: params.cfg,
agentDir,
modelProvider: params.modelProvider,
modelId: params.modelId,
});