fix(tools): allow no-tool llm-task runs

This commit is contained in:
Alex Knight
2026-05-03 22:52:12 +10:00
parent 0459bff556
commit 9c3e5f773e
4 changed files with 44 additions and 8 deletions

View File

@@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai
- Active Memory: preserve the target agent context when building embedded recall plugin tools so `memory_search` and `memory_get` stay available for explicit recall sessions. Fixes #76343. Thanks @Countermarch.
- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc.
- Plugins/install: allow official catalog-matched npm channel plugins such as Feishu to pass the trusted install scanner path while keeping spoofed package names blocked. Thanks @vincentkoc.
- Tools/llm-task: keep JSON-only embedded model runs from tripping inherited tool allowlists when tools are intentionally disabled, while preserving runtime `toolsAllow` failures. Fixes #74019. Thanks @amknight.
- Tools/profiles: make `tools.profile: "full"` grant all tools including optional plugin tools such as browser, so the full profile no longer silently drops plugin-provided tools that require an explicit allowlist entry. Fixes #76507. Thanks @amknight.
- Feishu: keep timeout env parsing separate from the HTTP client wrapper so package security scans no longer report a false env-harvesting hit during install. Thanks @vincentkoc.
- Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback.

View File

@@ -697,7 +697,7 @@ function collectAttemptExplicitToolAllowlistSources(params: {
{ label: "group tools.allow", allow: groupPolicy?.allow },
{ label: "sandbox tools.allow", allow: params.sandboxToolPolicy?.allow },
{ label: "subagent tools.allow", allow: subagentPolicy?.allow },
{ label: "runtime toolsAllow", allow: params.toolsAllow },
{ label: "runtime toolsAllow", allow: params.toolsAllow, enforceWhenToolsDisabled: true },
]);
}

View File

@@ -19,7 +19,9 @@ describe("tool allowlist guard", () => {
it("fails closed for runtime toolsAllow when tools are disabled", () => {
const error = buildEmptyExplicitToolAllowlistError({
sources: [{ label: "runtime toolsAllow", entries: ["query_db"] }],
sources: [
{ label: "runtime toolsAllow", entries: ["query_db"], enforceWhenToolsDisabled: true },
],
callableToolNames: [],
toolsEnabled: true,
disableTools: true,
@@ -29,6 +31,17 @@ describe("tool allowlist guard", () => {
expect(error?.message).toContain("tools are disabled for this run");
});
it("allows inherited config allowlists when a run intentionally disables tools", () => {
expect(
buildEmptyExplicitToolAllowlistError({
sources: [{ label: "tools.allow", entries: ["lobster", "llm-task"] }],
callableToolNames: [],
toolsEnabled: true,
disableTools: true,
}),
).toBeNull();
});
it("fails closed when the selected model cannot use requested tools", () => {
const error = buildEmptyExplicitToolAllowlistError({
sources: [{ label: "agents.db.tools.allow", entries: ["query_db"] }],
@@ -63,13 +76,21 @@ describe("tool allowlist guard", () => {
it("keeps source labels for config and runtime allowlists", () => {
const sources = collectExplicitToolAllowlistSources([
{ label: "tools.allow", allow: [" read ", ""] },
{ label: "runtime toolsAllow", allow: ["query_db"] },
{
label: "runtime toolsAllow",
allow: ["query_db"],
enforceWhenToolsDisabled: true,
},
{ label: "tools.byProvider.allow" },
]);
expect(sources).toEqual([
{ label: "tools.allow", entries: ["read"] },
{ label: "runtime toolsAllow", entries: ["query_db"] },
{
label: "runtime toolsAllow",
entries: ["query_db"],
enforceWhenToolsDisabled: true,
},
]);
});
});

View File

@@ -3,14 +3,24 @@ import { normalizeToolName } from "./tool-policy.js";
type ExplicitToolAllowlistSource = {
label: string;
entries: string[];
enforceWhenToolsDisabled?: boolean;
};
export function collectExplicitToolAllowlistSources(
sources: Array<{ label: string; allow?: string[] }>,
sources: Array<{ label: string; allow?: string[]; enforceWhenToolsDisabled?: boolean }>,
): ExplicitToolAllowlistSource[] {
return sources.flatMap((source) => {
const entries = (source.allow ?? []).map((entry) => entry.trim()).filter(Boolean);
return entries.length ? [{ label: source.label, entries }] : [];
if (entries.length === 0) {
return [];
}
return [
{
label: source.label,
entries,
...(source.enforceWhenToolsDisabled === true ? { enforceWhenToolsDisabled: true } : {}),
},
];
});
}
@@ -20,11 +30,15 @@ export function buildEmptyExplicitToolAllowlistError(params: {
toolsEnabled: boolean;
disableTools?: boolean;
}): Error | null {
const sources =
params.disableTools === true
? params.sources.filter((source) => source.enforceWhenToolsDisabled === true)
: params.sources;
const callableToolNames = params.callableToolNames.map(normalizeToolName).filter(Boolean);
if (params.sources.length === 0 || callableToolNames.length > 0) {
if (sources.length === 0 || callableToolNames.length > 0) {
return null;
}
const requested = params.sources
const requested = sources
.map((source) => `${source.label}: ${source.entries.map(normalizeToolName).join(", ")}`)
.join("; ");
const reason =