fix: restore Pi embedded tool allowlist

Restore the Pi embedded session tool allowlist for OpenAI/OpenAI Codex GPT-5 runs and compaction sessions after Pi 0.68.1 began treating session tools as a global allowlist.

Local validation: pnpm check:changed.
GitHub validation: check/check-additional/node shards green; parity gate red on unrelated config.patch stale/rate-limit QA harness scenario after plugins.allow restart.
This commit is contained in:
Josh Lehman
2026-04-22 12:51:42 -07:00
committed by GitHub
parent 78d491d909
commit ccc99d85bf
6 changed files with 101 additions and 16 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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"]);
});
});

View File

@@ -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<string>, 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<string> {
const names = new Set<string>();
for (const tool of tools) {
addName(names, tool.name);
}
return names;
}
export function toSessionToolAllowlist(allowedToolNames: Iterable<string>): string[] {
return [...new Set(allowedToolNames)].toSorted((a, b) => a.localeCompare(b));
}

View File

@@ -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`);