From aca258a8a9c3b3ca34ecc3bfc754e6935a997094 Mon Sep 17 00:00:00 2001 From: Galin Iliev Date: Sat, 16 May 2026 23:25:00 -0700 Subject: [PATCH] fix: explain memory compaction tool allowlist warnings Fixes #82941. --- CHANGELOG.md | 1 + src/agents/pi-tools.ts | 19 +++++++++-- src/agents/tool-policy-pipeline.test.ts | 14 +++++++++ src/agents/tool-policy-pipeline.ts | 42 ++++++++++++++++++++++--- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c1b0ea0c2..10299e5d912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - CLI/plugins: have `openclaw plugins doctor` warn when a configured runtime needs a missing owner plugin, sharing the same install mapping as `openclaw doctor --fix`. Fixes #81326. (#81674) Thanks @Zavianx. - Agents/Codex: route OpenAI runs that resolve to `openai-codex` through the Codex provider and bootstrap OpenClaw's stored OAuth profile into the Codex harness when the harness owns transport, so `openai/*` model refs no longer fail with `No API key found for openai-codex` despite an existing Codex OAuth profile. (#82864) Thanks @ragesaq. - Agents/ACP: distinguish prompt-submitted and runtime-active child stalls from true interactive waits, including redacted proxy-env diagnostics for Codex ACP no-output runs. Fixes #44810. +- Agents/memory: explain that memory-triggered compaction exposes only `read` and append-only `write` when configured core tools are unavailable in `tools.allow` warnings. Fixes #82941. Thanks @galiniliev. - Agents/skills: apply the full effective tool policy pipeline to inline `command-dispatch: tool` skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525) - Channel accounts: keep top-level default channel accounts visible when named accounts are added alongside default credential material, so mixed legacy/new account configs keep resolving `default` instead of silently dropping it. - Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram `/verbose` stays visible when command events arrive only at completion. diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 8cd8dfa8374..ddd6b76f595 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -990,6 +990,10 @@ export function createOpenClawCodingTools(options?: { toolsForMemoryFlush.push(tool); } } + const unavailableCoreToolReason = + isMemoryFlushRun && memoryFlushWritePath + ? "memory-triggered compaction runs expose only read and append-only write" + : undefined; const toolsForMessageProvider = filterToolsByMessageProvider( toolsForMemoryFlush, options?.messageProvider, @@ -1031,10 +1035,19 @@ export function createOpenClawCodingTools(options?: { groupPolicy: groupPolicyWithToolSearchControls, senderPolicy: senderPolicyWithToolSearchControls, agentId, + unavailableCoreToolReason, }), - { policy: sandboxToolPolicyWithToolSearchControls, label: "sandbox tools.allow" }, - { policy: subagentPolicyWithToolSearchControls, label: "subagent tools.allow" }, - { policy: inheritedToolPolicy, label: "inherited tools" }, + { + policy: sandboxToolPolicyWithToolSearchControls, + label: "sandbox tools.allow", + unavailableCoreToolReason, + }, + { + policy: subagentPolicyWithToolSearchControls, + label: "subagent tools.allow", + unavailableCoreToolReason, + }, + { policy: inheritedToolPolicy, label: "inherited tools", unavailableCoreToolReason }, ], }); if (shouldInheritEffectiveToolAllowlist) { diff --git a/src/agents/tool-policy-pipeline.test.ts b/src/agents/tool-policy-pipeline.test.ts index bf8fe0677c0..e92624e6fe5 100644 --- a/src/agents/tool-policy-pipeline.test.ts +++ b/src/agents/tool-policy-pipeline.test.ts @@ -13,6 +13,7 @@ function runAllowlistWarningStep(params: { label: string; suppressUnavailableCoreToolWarning?: boolean; suppressUnavailableCoreToolWarningAllowlist?: string[]; + unavailableCoreToolReason?: string; }) { const warnings: string[] = []; const tools = [{ name: "exec" }] as unknown as DummyTool[]; @@ -28,6 +29,7 @@ function runAllowlistWarningStep(params: { suppressUnavailableCoreToolWarning: params.suppressUnavailableCoreToolWarning, suppressUnavailableCoreToolWarningAllowlist: params.suppressUnavailableCoreToolWarningAllowlist, + unavailableCoreToolReason: params.unavailableCoreToolReason, }, ], }); @@ -107,6 +109,18 @@ describe("tool-policy-pipeline", () => { ]); }); + test("includes the active reason for unavailable core tool warnings", () => { + const warnings = runAllowlistWarningStep({ + allow: ["apply_patch", "wat"], + label: "tools.allow", + unavailableCoreToolReason: + "memory-triggered compaction runs expose only read and append-only write", + }); + expect(warnings).toEqual([ + "tools: tools.allow allowlist contains unknown entries (apply_patch, wat). Some entries are shipped core tools but unavailable here: memory-triggered compaction runs expose only read and append-only write; other entries won't match any tool unless the plugin is enabled.", + ]); + }); + test("default profile steps suppress unavailable baseline profile entries", () => { const warnings: string[] = []; const profilePolicy = resolveToolProfilePolicy("coding"); diff --git a/src/agents/tool-policy-pipeline.ts b/src/agents/tool-policy-pipeline.ts index 6b56361970b..fe926f88793 100644 --- a/src/agents/tool-policy-pipeline.ts +++ b/src/agents/tool-policy-pipeline.ts @@ -34,6 +34,7 @@ export type ToolPolicyPipelineStep = { stripPluginOnlyAllowlist?: boolean; suppressUnavailableCoreToolWarning?: boolean; suppressUnavailableCoreToolWarningAllowlist?: string[]; + unavailableCoreToolReason?: string; }; export function buildDefaultToolPolicyPipelineSteps(params: { @@ -50,16 +51,19 @@ export function buildDefaultToolPolicyPipelineSteps(params: { groupPolicy?: ToolPolicyLike; senderPolicy?: ToolPolicyLike; agentId?: string; + unavailableCoreToolReason?: string; }): ToolPolicyPipelineStep[] { const agentId = params.agentId?.trim(); const profile = params.profile?.trim(); const providerProfile = params.providerProfile?.trim(); + const unavailableCoreToolReason = params.unavailableCoreToolReason?.trim(); return [ { policy: params.profilePolicy, label: profile ? `tools.profile (${profile})` : "tools.profile", stripPluginOnlyAllowlist: true, suppressUnavailableCoreToolWarningAllowlist: params.profileUnavailableCoreWarningAllowlist, + unavailableCoreToolReason, }, { policy: params.providerProfilePolicy, @@ -69,25 +73,44 @@ export function buildDefaultToolPolicyPipelineSteps(params: { stripPluginOnlyAllowlist: true, suppressUnavailableCoreToolWarningAllowlist: params.providerProfileUnavailableCoreWarningAllowlist, + unavailableCoreToolReason, + }, + { + policy: params.globalPolicy, + label: "tools.allow", + stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, }, - { policy: params.globalPolicy, label: "tools.allow", stripPluginOnlyAllowlist: true }, { policy: params.globalProviderPolicy, label: "tools.byProvider.allow", stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, }, { policy: params.agentPolicy, label: agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow", stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, }, { policy: params.agentProviderPolicy, label: agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow", stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, + }, + { + policy: params.groupPolicy, + label: "group tools.allow", + stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, + }, + { + policy: params.senderPolicy, + label: "tools.toolsBySender", + stripPluginOnlyAllowlist: true, + unavailableCoreToolReason, }, - { policy: params.groupPolicy, label: "group tools.allow", stripPluginOnlyAllowlist: true }, - { policy: params.senderPolicy, label: "tools.toolsBySender", stripPluginOnlyAllowlist: true }, ]; } @@ -145,6 +168,7 @@ export function applyToolPolicyPipeline(params: { pluginOnlyAllowlist: resolved.pluginOnlyAllowlist, hasGatedCoreEntries: warnableGatedCoreEntries.length > 0, hasOtherEntries: otherEntries.length > 0, + unavailableCoreToolReason: step.unavailableCoreToolReason, }); const warning = `tools: ${step.label} allowlist contains unknown entries (${entries}). ${suffix}`; if (rememberToolPolicyWarning(warning)) { @@ -172,15 +196,23 @@ function describeUnknownAllowlistSuffix(params: { pluginOnlyAllowlist: boolean; hasGatedCoreEntries: boolean; hasOtherEntries: boolean; + unavailableCoreToolReason?: string; }): string { const preface = params.pluginOnlyAllowlist ? "Allowlist contains only plugin entries; core tools will not be available." : ""; + const unavailableCoreToolReason = params.unavailableCoreToolReason?.trim(); + const unavailableCoreDetail = unavailableCoreToolReason + ? `These entries are shipped core tools but unavailable here: ${unavailableCoreToolReason}.` + : "These entries are shipped core tools but unavailable in the current runtime/provider/model/config."; + const mixedUnavailableCoreDetail = unavailableCoreToolReason + ? `Some entries are shipped core tools but unavailable here: ${unavailableCoreToolReason}; other entries won't match any tool unless the plugin is enabled.` + : "Some entries are shipped core tools but unavailable in the current runtime/provider/model/config; other entries won't match any tool unless the plugin is enabled."; const detail = params.hasGatedCoreEntries && params.hasOtherEntries - ? "Some entries are shipped core tools but unavailable in the current runtime/provider/model/config; other entries won't match any tool unless the plugin is enabled." + ? mixedUnavailableCoreDetail : params.hasGatedCoreEntries - ? "These entries are shipped core tools but unavailable in the current runtime/provider/model/config." + ? unavailableCoreDetail : "These entries won't match any tool unless the plugin is enabled."; return preface ? `${preface} ${detail}` : detail; }