fix: restore Anthropic OAuth tool dispatch

This commit is contained in:
Peter Steinberger
2026-01-10 04:01:00 +01:00
parent 8e63cd9a76
commit 8466e53b5d
11 changed files with 190 additions and 144 deletions

View File

@@ -117,10 +117,10 @@
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.42.1",
"@mariozechner/pi-ai": "^0.42.1",
"@mariozechner/pi-coding-agent": "^0.42.1",
"@mariozechner/pi-tui": "^0.42.1",
"@mariozechner/pi-agent-core": "^0.42.2",
"@mariozechner/pi-ai": "^0.42.2",
"@mariozechner/pi-coding-agent": "^0.42.2",
"@mariozechner/pi-tui": "^0.42.2",
"@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
@@ -190,7 +190,7 @@
"patchedDependencies": {
"@buape/carbon@0.0.0-beta-20260109194934": "patches/@buape__carbon@0.0.0-beta-20260109194934.patch",
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
"@mariozechner/pi-ai@0.42.1": "patches/@mariozechner__pi-ai@0.42.1.patch",
"@mariozechner/pi-ai@0.42.2": "patches/@mariozechner__pi-ai@0.42.2.patch",
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
"playwright-core@1.57.0": "patches/playwright-core@1.57.0.patch",
"qrcode-terminal": "patches/qrcode-terminal.patch"

View File

@@ -0,0 +1,57 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index 93aa26c395e9bd0df64376408a13d15ee9e7cce7..41a439e5fc370038a5febef9e8f021ee279cf8aa 100644
--- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js
@@ -248,6 +248,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
break; // Success, exit retry loop
}
const errorText = await response.text();
+ // Fail immediately on 429 for Antigravity to let callers rotate accounts.
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
+ if (isAntigravity && response.status === 429) {
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
+ }
// Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
index 188a8294f26fe1bfe3fb298a7f58e4d8eaf2a529..3fd8027edafdad4ca364af53f0a1811139705b21 100644
--- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js
@@ -433,9 +433,15 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`.
+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but
+ // their stored reasoning items must not be replayed as standalone `reasoning` input.
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
for (const block of msg.content) {
if (block.type === "thinking" && msg.stopReason !== "error") {
if (block.thinkingSignature) {
+ if (!hasTextBlock)
+ continue;
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..0bf46bfb4a6fac5a0304652e42566b2c991bab48 100644
--- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js
@@ -396,10 +396,16 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`.
+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but
+ // their stored reasoning items must not be replayed as standalone `reasoning` input.
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
for (const block of msg.content) {
// Do not submit thinking blocks if the completion had an error (i.e. abort)
if (block.type === "thinking" && msg.stopReason !== "error") {
if (block.thinkingSignature) {
+ if (!hasTextBlock)
+ continue;
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}

66
pnpm-lock.yaml generated
View File

@@ -14,13 +14,13 @@ patchedDependencies:
'@mariozechner/pi-agent-core':
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
path: patches/@mariozechner__pi-agent-core.patch
'@mariozechner/pi-ai@0.42.1':
'@mariozechner/pi-ai@0.42.2':
hash: 0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c
path: patches/@mariozechner__pi-ai@0.42.1.patch
path: patches/@mariozechner__pi-ai@0.42.2.patch
'@mariozechner/pi-coding-agent':
hash: 58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e
path: patches/@mariozechner__pi-coding-agent.patch
'playwright-core@1.57.0':
playwright-core@1.57.0:
hash: 66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02
path: patches/playwright-core@1.57.0.patch
qrcode-terminal:
@@ -47,17 +47,17 @@ importers:
specifier: ^1.3.4
version: 1.3.4
'@mariozechner/pi-agent-core':
specifier: ^0.42.1
version: 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
specifier: ^0.42.2
version: 0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai':
specifier: ^0.42.1
version: 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
specifier: ^0.42.2
version: 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-coding-agent':
specifier: ^0.42.1
version: 0.42.1(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)
specifier: ^0.42.2
version: 0.42.2(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui':
specifier: ^0.42.1
version: 0.42.1
specifier: ^0.42.2
version: 0.42.2
'@microsoft/agents-hosting':
specifier: ^1.1.1
version: 1.1.1
@@ -132,13 +132,13 @@ importers:
version: 0.2.0
playwright-core:
specifier: 1.57.0
version: 1.57.0
version: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02)
proper-lockfile:
specifier: ^4.1.2
version: 4.1.2
qrcode-terminal:
specifier: ^0.12.0
version: 0.12.0
version: 0.12.0(patch_hash=ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12)
sharp:
specifier: ^0.34.5
version: 0.34.5
@@ -860,22 +860,22 @@ packages:
peerDependencies:
lit: ^3.3.1
'@mariozechner/pi-agent-core@0.42.1':
resolution: {integrity: sha512-sIRB1jHheQSGONoVorrQ3X9vc1JFLoAe+48k7UJmpLAhJXnQmRtq1+CtzvKOVe5qVRyNBNuNdXipH+seicbwvQ==}
'@mariozechner/pi-agent-core@0.42.2':
resolution: {integrity: sha512-j81u9v6FhNgYXTTcgM5FB7f2hLQPc/73oTM0LuRDlybJakDRM3z/BBGkRk5csCfjYN1OeEGMjqj61pR30WqOZg==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-ai@0.42.1':
resolution: {integrity: sha512-uRiH+s7EPDz9Q7hjQJ4Mm8lU4e4/C8pB4rnqGds73B5/0rqZb8DeSBukKU6uOwMNkXol/tYgnkEqXy7qEoHLJQ==}
'@mariozechner/pi-ai@0.42.2':
resolution: {integrity: sha512-uIPfOCGSWm8Uo4kJ0nWKClJlIhBFxDasBbbAzzEgR9NrsyZMHuFYn4Y2XsbNKHmc4KnoS6DZvaO6IP+/+IS9rw==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-coding-agent@0.42.1':
resolution: {integrity: sha512-BsLgFBC//98EdQCCoNYPfxKi2TEJ64a9w/CQC8/DyeAVhjrdJELUd+3EoRV6iiodbpUspUASW9w3ELoqQGAtoA==}
'@mariozechner/pi-coding-agent@0.42.2':
resolution: {integrity: sha512-Rlxo2rWU4RjwBe4jr/CnqqknbDF3fnb3RzCe9u1mMm4nb2T9qLP3QGDo2c5DhWEpcRZ54hrR6HTennlEazfXvA==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-tui@0.42.1':
resolution: {integrity: sha512-dsmzcOjD+9MNgqnJ6/BJ255Sc9KQWN4Asyh1ron7bMA4Q9lqSWl6q8dk78DBp2QcVeWHtvqbwdZwNnDwWyxw3g==}
'@mariozechner/pi-tui@0.42.2':
resolution: {integrity: sha512-im3HwwKvSlh+N1hJpKVoZYW1JrXz0N3eN/PfpWX8DJYYTTG4RDaT0Mbbf0vUbkKSqCfeJQZCN5bfCocTXlU8wA==}
engines: {node: '>=20.0.0'}
'@microsoft/agents-activity@1.1.1':
@@ -3790,10 +3790,10 @@ snapshots:
transitivePeerDependencies:
- tailwindcss
'@mariozechner/pi-agent-core@0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-agent-core@0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/pi-ai': 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.42.1
'@mariozechner/pi-ai': 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.42.2
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
@@ -3802,7 +3802,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-ai@0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
'@google/genai': 1.34.0
@@ -3822,12 +3822,12 @@ snapshots:
- ws
- zod
'@mariozechner/pi-coding-agent@0.42.1(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-coding-agent@0.42.2(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/clipboard': 0.3.0
'@mariozechner/pi-agent-core': 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.42.1
'@mariozechner/pi-agent-core': 0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.42.2
chalk: 5.6.2
cli-highlight: 2.1.11
diff: 8.0.2
@@ -3846,7 +3846,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-tui@0.42.1':
'@mariozechner/pi-tui@0.42.2':
dependencies:
'@types/mime-types': 2.1.4
chalk: 5.6.2
@@ -5689,11 +5689,11 @@ snapshots:
dependencies:
pngjs: 7.0.0
playwright-core@1.57.0: {}
playwright-core@1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02): {}
playwright@1.57.0:
dependencies:
playwright-core: 1.57.0
playwright-core: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02)
optionalDependencies:
fsevents: 2.3.2
@@ -5778,7 +5778,7 @@ snapshots:
'@thi.ng/bitstream': 2.4.37
optional: true
qrcode-terminal@0.12.0: {}
qrcode-terminal@0.12.0(patch_hash=ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12): {}
qs@6.14.1:
dependencies:

View File

@@ -44,7 +44,7 @@ const DEFAULT_PATH =
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
// Type.Union of literals compiles to { anyOf: [{enum:["a"]}, {enum:["b"]}, ...] }
// which is valid but not accepted. A flat enum { type: "string", enum: [...] } works.
const stringEnum = <T extends readonly string[]>(
const _stringEnum = <T extends readonly string[]>(
values: T,
options?: { description?: string },
) =>
@@ -453,12 +453,7 @@ export function createBashTool(
export const bashTool = createBashTool();
const processSchema = Type.Object({
action: stringEnum(
["list", "poll", "log", "write", "kill", "clear", "remove"] as const,
{
description: "Process action",
},
),
action: Type.String({ description: "Process action" }),
sessionId: Type.Optional(
Type.String({ description: "Session id for actions other than list" }),
),

View File

@@ -118,12 +118,11 @@ function createStubTool(name: string): AgentTool {
}
describe("splitSdkTools", () => {
// Tool names are now capitalized (Bash, Read, etc.) to bypass Anthropic OAuth blocking
const tools = [
createStubTool("Read"),
createStubTool("Bash"),
createStubTool("Edit"),
createStubTool("Write"),
createStubTool("read"),
createStubTool("bash"),
createStubTool("edit"),
createStubTool("write"),
createStubTool("browser"),
];
@@ -134,27 +133,25 @@ describe("splitSdkTools", () => {
});
expect(builtInTools).toEqual([]);
expect(customTools.map((tool) => tool.name)).toEqual([
"Read",
"Bash",
"Edit",
"Write",
"read",
"bash",
"edit",
"write",
"browser",
]);
});
it("routes all tools to customTools even when not sandboxed (for OAuth compatibility)", () => {
// All tools are now passed as customTools to bypass pi-coding-agent's
// built-in tool filtering, which expects lowercase names.
it("routes all tools to customTools even when not sandboxed", () => {
const { builtInTools, customTools } = splitSdkTools({
tools,
sandboxEnabled: false,
});
expect(builtInTools).toEqual([]);
expect(customTools.map((tool) => tool.name)).toEqual([
"Read",
"Bash",
"Edit",
"Write",
"read",
"bash",
"edit",
"write",
"browser",
]);
});

View File

@@ -605,10 +605,8 @@ export function createSystemPromptOverride(
return () => trimmed;
}
// Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's
// OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has
// hardcoded lowercase names in its built-in tool registry, so we must pass ALL
// tools as customTools to bypass the SDK's filtering.
// We always pass tools via `customTools` so our policy filtering, sandbox integration,
// and extended toolset remain consistent across providers.
type AnyAgentTool = AgentTool;
@@ -619,9 +617,8 @@ export function splitSdkTools(options: {
builtInTools: AnyAgentTool[];
customTools: ReturnType<typeof toToolDefinitions>;
} {
// Always pass all tools as customTools to bypass pi-coding-agent's built-in
// tool filtering, which expects lowercase names (bash, read, write, edit).
// Our tools are now capitalized (Bash, Read, Write, Edit) for OAuth compatibility.
// Always pass all tools as customTools so the SDK doesn't "helpfully" swap in
// its own built-in implementations (we need our tool wrappers + policy).
const { tools } = options;
return {
builtInTools: [],

View File

@@ -28,9 +28,9 @@ describe("Agent-specific tool filtering", () => {
});
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("Read");
expect(toolNames).toContain("Write");
expect(toolNames).not.toContain("Bash");
expect(toolNames).toContain("read");
expect(toolNames).toContain("write");
expect(toolNames).not.toContain("bash");
});
it("should keep global tool policy when agent only sets tools.elevated", () => {
@@ -62,9 +62,9 @@ describe("Agent-specific tool filtering", () => {
});
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("Bash");
expect(toolNames).toContain("Read");
expect(toolNames).not.toContain("Write");
expect(toolNames).toContain("bash");
expect(toolNames).toContain("read");
expect(toolNames).not.toContain("write");
});
it("should apply agent-specific tool policy", () => {
@@ -95,10 +95,10 @@ describe("Agent-specific tool filtering", () => {
});
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("Read");
expect(toolNames).not.toContain("Bash");
expect(toolNames).not.toContain("Write");
expect(toolNames).not.toContain("Edit");
expect(toolNames).toContain("read");
expect(toolNames).not.toContain("bash");
expect(toolNames).not.toContain("write");
expect(toolNames).not.toContain("edit");
});
it("should allow different tool policies for different agents", () => {
@@ -130,9 +130,9 @@ describe("Agent-specific tool filtering", () => {
agentDir: "/tmp/agent-main",
});
const mainToolNames = mainTools.map((t) => t.name);
expect(mainToolNames).toContain("Bash");
expect(mainToolNames).toContain("Write");
expect(mainToolNames).toContain("Edit");
expect(mainToolNames).toContain("bash");
expect(mainToolNames).toContain("write");
expect(mainToolNames).toContain("edit");
// family agent: restricted
const familyTools = createClawdbotCodingTools({
@@ -142,10 +142,10 @@ describe("Agent-specific tool filtering", () => {
agentDir: "/tmp/agent-family",
});
const familyToolNames = familyTools.map((t) => t.name);
expect(familyToolNames).toContain("Read");
expect(familyToolNames).not.toContain("Bash");
expect(familyToolNames).not.toContain("Write");
expect(familyToolNames).not.toContain("Edit");
expect(familyToolNames).toContain("read");
expect(familyToolNames).not.toContain("bash");
expect(familyToolNames).not.toContain("write");
expect(familyToolNames).not.toContain("edit");
});
it("should prefer agent-specific tool policy over global", () => {
@@ -176,7 +176,7 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name);
// Agent policy overrides global: browser is allowed again
expect(toolNames).toContain("browser");
expect(toolNames).not.toContain("Bash");
expect(toolNames).not.toContain("bash");
expect(toolNames).not.toContain("process");
});
@@ -247,9 +247,9 @@ describe("Agent-specific tool filtering", () => {
// Agent policy should be applied first, then sandbox
// Agent allows only "read", sandbox allows ["read", "write", "bash"]
// Result: only "read" (most restrictive wins)
expect(toolNames).toContain("Read");
expect(toolNames).not.toContain("Bash");
expect(toolNames).not.toContain("Write");
expect(toolNames).toContain("read");
expect(toolNames).not.toContain("bash");
expect(toolNames).not.toContain("write");
});
it("should run bash synchronously when process is denied", async () => {
@@ -265,7 +265,7 @@ describe("Agent-specific tool filtering", () => {
workspaceDir: "/tmp/test-main",
agentDir: "/tmp/agent-main",
});
const bash = tools.find((tool) => tool.name === "Bash");
const bash = tools.find((tool) => tool.name === "bash");
expect(bash).toBeDefined();
const result = await bash?.execute("call1", {

View File

@@ -170,8 +170,7 @@ describe("createClawdbotCodingTools", () => {
it("includes bash and process tools", () => {
const tools = createClawdbotCodingTools();
// NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
expect(tools.some((tool) => tool.name === "process")).toBe(true);
});
@@ -213,9 +212,8 @@ describe("createClawdbotCodingTools", () => {
expect(names.has("sessions_send")).toBe(false);
expect(names.has("sessions_spawn")).toBe(false);
// NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(names.has("Read")).toBe(true);
expect(names.has("Bash")).toBe(true);
expect(names.has("read")).toBe(true);
expect(names.has("bash")).toBe(true);
expect(names.has("process")).toBe(true);
});
@@ -234,14 +232,12 @@ describe("createClawdbotCodingTools", () => {
},
},
});
// Tool names are capitalized for OAuth compatibility
expect(tools.map((tool) => tool.name)).toEqual(["Read"]);
expect(tools.map((tool) => tool.name)).toEqual(["read"]);
});
it("keeps read tool image metadata intact", async () => {
const tools = createClawdbotCodingTools();
// NOTE: read is capitalized to bypass Anthropic OAuth blocking
const readTool = tools.find((tool) => tool.name === "Read");
const readTool = tools.find((tool) => tool.name === "read");
expect(readTool).toBeDefined();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
@@ -281,8 +277,7 @@ describe("createClawdbotCodingTools", () => {
it("returns text content without image blocks for text files", async () => {
const tools = createClawdbotCodingTools();
// NOTE: read is capitalized to bypass Anthropic OAuth blocking
const readTool = tools.find((tool) => tool.name === "Read");
const readTool = tools.find((tool) => tool.name === "read");
expect(readTool).toBeDefined();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
@@ -337,10 +332,8 @@ describe("createClawdbotCodingTools", () => {
},
};
const tools = createClawdbotCodingTools({ sandbox });
// NOTE: bash/read are capitalized to bypass Anthropic OAuth blocking
// Policy matching is case-insensitive, so allow: ["bash"] matches tool named "Bash"
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "Read")).toBe(false);
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
expect(tools.some((tool) => tool.name === "read")).toBe(false);
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
});
@@ -370,18 +363,16 @@ describe("createClawdbotCodingTools", () => {
},
};
const tools = createClawdbotCodingTools({ sandbox });
// NOTE: read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "Read")).toBe(true);
expect(tools.some((tool) => tool.name === "Write")).toBe(false);
expect(tools.some((tool) => tool.name === "Edit")).toBe(false);
expect(tools.some((tool) => tool.name === "read")).toBe(true);
expect(tools.some((tool) => tool.name === "write")).toBe(false);
expect(tools.some((tool) => tool.name === "edit")).toBe(false);
});
it("filters tools by agent tool policy even without sandbox", () => {
const tools = createClawdbotCodingTools({
config: { tools: { deny: ["browser"] } },
});
// NOTE: bash is capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
});

View File

@@ -283,28 +283,6 @@ function normalizeToolNames(list?: string[]) {
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
}
/**
* Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
* Renaming to capitalized versions bypasses the block while maintaining compatibility
* with regular API keys.
*/
const OAUTH_BLOCKED_TOOL_NAMES: Record<string, string> = {
bash: "Bash",
read: "Read",
write: "Write",
edit: "Edit",
};
function renameBlockedToolsForOAuth(tools: AnyAgentTool[]): AnyAgentTool[] {
return tools.map((tool) => {
const newName = OAUTH_BLOCKED_TOOL_NAMES[tool.name];
if (newName) {
return { ...tool, name: newName };
}
return tool;
});
}
const DEFAULT_SUBAGENT_TOOL_DENY = [
"sessions_list",
"sessions_history",
@@ -656,7 +634,8 @@ export function createClawdbotCodingTools(options?: {
)
: normalized;
// Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
// Always use capitalized versions for compatibility with both OAuth and regular API keys.
return renameBlockedToolsForOAuth(withAbort);
// NOTE: Keep canonical (lowercase) tool names here.
// pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names
// on the wire and maps them back for tool dispatch.
return withAbort;
}

View File

@@ -46,6 +46,21 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("sessions_send");
});
it("preserves tool casing in the prompt", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
toolNames: ["Read", "Bash", "process"],
skillsPrompt:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
});
expect(prompt).toContain("- Read: Read file contents");
expect(prompt).toContain("- Bash: Run shell commands");
expect(prompt).toContain(
"Use `Read` to load the SKILL.md at the location listed for that skill.",
);
});
it("includes user time when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",

View File

@@ -84,9 +84,19 @@ export function buildAgentSystemPrompt(params: {
"image",
];
const normalizedTools = (params.toolNames ?? [])
.map((tool) => tool.trim().toLowerCase())
.filter(Boolean);
const rawToolNames = (params.toolNames ?? []).map((tool) => tool.trim());
const canonicalToolNames = rawToolNames.filter(Boolean);
const canonicalByNormalized = new Map<string, string>();
for (const name of canonicalToolNames) {
const normalized = name.toLowerCase();
if (!canonicalByNormalized.has(normalized)) {
canonicalByNormalized.set(normalized, name);
}
}
const resolveToolName = (normalized: string) =>
canonicalByNormalized.get(normalized) ?? normalized;
const normalizedTools = canonicalToolNames.map((tool) => tool.toLowerCase());
const availableTools = new Set(normalizedTools);
const extraTools = Array.from(
new Set(normalizedTools.filter((tool) => !toolOrder.includes(tool))),
@@ -94,13 +104,17 @@ export function buildAgentSystemPrompt(params: {
const enabledTools = toolOrder.filter((tool) => availableTools.has(tool));
const toolLines = enabledTools.map((tool) => {
const summary = toolSummaries[tool];
return summary ? `- ${tool}: ${summary}` : `- ${tool}`;
const name = resolveToolName(tool);
return summary ? `- ${name}: ${summary}` : `- ${name}`;
});
for (const tool of extraTools.sort()) {
toolLines.push(`- ${tool}`);
toolLines.push(`- ${resolveToolName(tool)}`);
}
const hasGateway = availableTools.has("gateway");
const readToolName = resolveToolName("read");
const bashToolName = resolveToolName("bash");
const processToolName = resolveToolName("process");
const extraSystemPrompt = params.extraSystemPrompt?.trim();
const ownerNumbers = (params.ownerNumbers ?? [])
.map((value) => value.trim())
@@ -143,7 +157,7 @@ export function buildAgentSystemPrompt(params: {
const skillsSection = skillsPrompt
? [
"## Skills",
"Skills provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.",
`Skills provide task-specific instructions. Use \`${readToolName}\` to load the SKILL.md at the location listed for that skill.`,
...skillsLines,
"",
]
@@ -154,6 +168,7 @@ export function buildAgentSystemPrompt(params: {
"",
"## Tooling",
"Tool availability (filtered by policy):",
"Tool names are case-sensitive. Call tools exactly as listed.",
toolLines.length > 0
? toolLines.join("\n")
: [
@@ -161,8 +176,8 @@ export function buildAgentSystemPrompt(params: {
"- grep: search file contents for patterns",
"- find: find files by glob pattern",
"- ls: list directory contents",
"- bash: run shell commands (supports background via yieldMs/background)",
"- process: manage background bash sessions",
`- ${bashToolName}: run shell commands (supports background via yieldMs/background)`,
`- ${processToolName}: manage background bash sessions`,
"- whatsapp_login: generate a WhatsApp QR code and wait for linking",
"- browser: control clawd's dedicated browser",
"- canvas: present/eval/snapshot the Canvas",