diff --git a/CHANGELOG.md b/CHANGELOG.md index abc8b4cbdf0..6b752594221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ Docs: https://docs.openclaw.ai - Pairing: remove stale pending requests for a device when that paired device is deleted, so an old repair approval cannot recreate the removed device from leftover state. - Security/dotenv: block workspace `.env` overrides for Matrix, Mattermost, IRC, and Synology endpoint settings so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. (#70240) Thanks @drobison00. - Telegram: require the same `/models` authorization for group model-picker callbacks, so unauthorized participants can no longer browse or change the session model through inline buttons. (#70235) Thanks @drobison00. +- Agents/Pi: keep the filtered tool-name allowlist active for embedded OpenAI/OpenAI Codex GPT-5 runs and compaction sessions, so bundled and client tools still execute after the Pi `0.68.1` session-tool allowlist change instead of stopping at plan-only replies with no tool call. (#70281) Thanks @jalehman. ## 2026.4.21 diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index d312a5b343f..3f6788a14cb 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -132,7 +132,11 @@ import { buildEmbeddedSystemPrompt, createSystemPromptOverride, } from "./system-prompt.js"; -import { collectAllowedToolNames } from "./tool-name-allowlist.js"; +import { + collectAllowedToolNames, + collectRegisteredToolNames, + toSessionToolAllowlist, +} from "./tool-name-allowlist.js"; import { logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas, @@ -839,12 +843,15 @@ export async function compactEmbeddedPiSessionDirect( contextTokenBudget: ctxInfo.tokens, }); - const { builtInTools, customTools } = splitSdkTools({ + const { customTools } = splitSdkTools({ tools: effectiveTools, sandboxEnabled: !!sandbox?.enabled, }); - // OpenClaw registers filtered tools through `customTools`; keep Pi's - // built-in tool list empty so the SDK does not re-enable defaults. + // Pi 0.68.1 uses `tools` as a global allowlist across built-in and + // custom tools. Keep the built-in tool list empty, but still pass the + // exact registered custom-tool names so our OpenClaw-managed + // registrations remain active without broadening the session boundary. + const sessionToolAllowlist = toSessionToolAllowlist(collectRegisteredToolNames(customTools)); const providerStreamFn = resolveCompactionProviderStream({ effectiveModel, @@ -882,7 +889,7 @@ export async function compactEmbeddedPiSessionDirect( modelRegistry, model: effectiveModel, thinkingLevel: mapThinkingLevel(thinkLevel), - tools: builtInTools, + tools: sessionToolAllowlist, customTools, sessionManager, settingsManager, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 304598bc82e..bd338375353 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -167,7 +167,12 @@ import { createSystemPromptOverride, } from "../system-prompt.js"; import { dropThinkingBlocks } from "../thinking.js"; -import { collectAllowedToolNames } from "../tool-name-allowlist.js"; +import { + collectAllowedToolNames, + collectRegisteredToolNames, + PI_RESERVED_TOOL_NAMES, + toSessionToolAllowlist, +} from "../tool-name-allowlist.js"; import { installContextEngineLoopHook, installToolResultContextGuard, @@ -1062,7 +1067,7 @@ export async function runEmbeddedAttempt( // Get hook runner early so it's available when creating tools const hookRunner = getGlobalHookRunner(); - const { builtInTools, customTools } = splitSdkTools({ + const { customTools } = splitSdkTools({ tools: effectiveTools, sandboxEnabled: !!sandbox?.enabled, }); @@ -1099,7 +1104,7 @@ export async function runEmbeddedAttempt( ); const clientToolNameConflicts = findClientToolNameConflicts({ tools: clientTools ?? [], - existingToolNames: coreBuiltinToolNames, + existingToolNames: [...coreBuiltinToolNames, ...PI_RESERVED_TOOL_NAMES], }); if (clientToolNameConflicts.length > 0) { throw createClientToolNameConflictError(clientToolNameConflicts); @@ -1121,8 +1126,14 @@ export async function runEmbeddedAttempt( : []; const allCustomTools = [...customTools, ...clientToolDefs]; - // OpenClaw registers filtered tools through `customTools`; keep Pi's - // built-in tool list empty so the SDK does not re-enable defaults. + // Pi 0.68.1 uses `tools` as a global allowlist across built-in and + // custom tools. Keep the built-in tool list empty, but still pass the + // exact registered custom-tool names so our OpenClaw-managed + // registrations remain active without widening the session boundary to + // raw client-provided names. + const sessionToolAllowlist = toSessionToolAllowlist( + collectRegisteredToolNames(allCustomTools), + ); ({ session } = await createEmbeddedAgentSessionWithResourceLoader({ createAgentSession: async (options) => @@ -1134,7 +1145,7 @@ export async function runEmbeddedAttempt( modelRegistry: params.modelRegistry, model: params.model, thinkingLevel: mapThinkingLevel(params.thinkLevel), - tools: builtInTools, + tools: sessionToolAllowlist, customTools: allCustomTools, sessionManager, settingsManager, @@ -1315,10 +1326,9 @@ export async function runEmbeddedAttempt( } const cacheObservabilityEnabled = Boolean(cacheTrace) || log.isEnabled("debug"); - const promptCacheToolNames = collectPromptCacheToolNames([ - ...builtInTools, - ...allCustomTools, - ] as Array<{ name?: string }>); + const promptCacheToolNames = collectPromptCacheToolNames( + allCustomTools as Array<{ name?: string }>, + ); let promptCacheChangesForTurn: PromptCacheChange[] | null = null; if (cacheTrace) { diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts b/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts index d88e97fdffb..284f02ced30 100644 --- a/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts +++ b/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vitest"; import { createStubTool } from "../test-helpers/pi-tool-stubs.js"; -import { collectAllowedToolNames, toSessionToolAllowlist } from "./tool-name-allowlist.js"; +import { + collectAllowedToolNames, + collectRegisteredToolNames, + PI_RESERVED_TOOL_NAMES, + toSessionToolAllowlist, +} from "./tool-name-allowlist.js"; describe("tool name allowlists", () => { it("collects local and client tool names", () => { @@ -26,4 +31,40 @@ describe("tool name allowlists", () => { expect(allowlist).toEqual(["edit", "read", "write"]); }); + + it("collects exact registered custom-tool names for the Pi session allowlist", () => { + const allowlist = toSessionToolAllowlist( + collectRegisteredToolNames([ + { name: "exec" }, + { name: "read" }, + { name: "exec" }, + { name: "image_generate" }, + ]), + ); + + expect(allowlist).toEqual(["exec", "image_generate", "read"]); + }); + + it("pins the reserved Pi built-in tool namespace used by client conflict checks", () => { + expect(PI_RESERVED_TOOL_NAMES).toEqual(["bash", "edit", "find", "grep", "ls", "read", "write"]); + }); + + it("keeps collected run allowlists broader than the Pi session allowlist source", () => { + const allowlist = toSessionToolAllowlist( + collectAllowedToolNames({ + tools: [createStubTool("exec"), createStubTool("read"), createStubTool("exec")], + clientTools: [ + { + type: "function", + function: { + name: "image_generate", + parameters: { type: "object", properties: {} }, + }, + }, + ], + }), + ); + + expect(allowlist).toEqual(["exec", "image_generate", "read"]); + }); }); diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.ts b/src/agents/pi-embedded-runner/tool-name-allowlist.ts index eded5ac1968..e5346ea622d 100644 --- a/src/agents/pi-embedded-runner/tool-name-allowlist.ts +++ b/src/agents/pi-embedded-runner/tool-name-allowlist.ts @@ -1,6 +1,12 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { ClientToolDefinition } from "./run/params.js"; +/** + * Pi built-in tools that remain present in the embedded runtime even when + * OpenClaw routes execution through custom tool definitions. + */ +export const PI_RESERVED_TOOL_NAMES = ["bash", "edit", "find", "grep", "ls", "read", "write"]; + function addName(names: Set, value: unknown): void { if (typeof value !== "string") { return; @@ -25,6 +31,17 @@ export function collectAllowedToolNames(params: { return names; } +/** + * Collect the exact tool names registered with Pi for this session. + */ +export function collectRegisteredToolNames(tools: Array<{ name?: string }>): Set { + const names = new Set(); + for (const tool of tools) { + addName(names, tool.name); + } + return names; +} + export function toSessionToolAllowlist(allowedToolNames: Iterable): string[] { return [...new Set(allowedToolNames)].toSorted((a, b) => a.localeCompare(b)); } diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/pi-tool-definition-adapter.test.ts index af88ef37ef4..b3c4c8a9c52 100644 --- a/src/agents/pi-tool-definition-adapter.test.ts +++ b/src/agents/pi-tool-definition-adapter.test.ts @@ -203,6 +203,15 @@ describe("client tool name conflict checks", () => { ).toEqual(["Weather", "weather"]); }); + it("detects collisions with reserved Pi built-in tool names", () => { + expect( + findClientToolNameConflicts({ + tools: [makeClientTool("Bash"), makeClientTool("grep")], + existingToolNames: ["bash", "edit", "find", "grep", "ls", "read", "write"], + }), + ).toEqual(["Bash", "grep"]); + }); + it("wraps conflict errors with a stable prefix", () => { const err = createClientToolNameConflictError(["exec", "Web_Search"]); expect(err.message).toBe(`${CLIENT_TOOL_NAME_CONFLICT_PREFIX} exec, Web_Search`);