fix: bypass Anthropic OAuth token blocking for tool names
Anthropic blocks specific lowercase tool names (bash, read, write, edit) when using OAuth tokens. This fix: 1. Renames blocked tools to capitalized versions (Bash, Read, Write, Edit) in pi-tools.ts via renameBlockedToolsForOAuth() 2. Passes all tools as customTools in splitSdkTools() to bypass pi-coding-agent's built-in tool filtering, which expects lowercase names The capitalized names work with both OAuth tokens and regular API keys. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
a69a863090
commit
333832c2e1
@@ -68,41 +68,45 @@ function createStubTool(name: string): AgentTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("splitSdkTools", () => {
|
describe("splitSdkTools", () => {
|
||||||
|
// Tool names are now capitalized (Bash, Read, etc.) to bypass Anthropic OAuth blocking
|
||||||
const tools = [
|
const tools = [
|
||||||
createStubTool("read"),
|
createStubTool("Read"),
|
||||||
createStubTool("bash"),
|
createStubTool("Bash"),
|
||||||
createStubTool("edit"),
|
createStubTool("Edit"),
|
||||||
createStubTool("write"),
|
createStubTool("Write"),
|
||||||
createStubTool("browser"),
|
createStubTool("browser"),
|
||||||
];
|
];
|
||||||
|
|
||||||
it("routes built-ins to custom tools when sandboxed", () => {
|
it("routes all tools to customTools when sandboxed", () => {
|
||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: true,
|
sandboxEnabled: true,
|
||||||
});
|
});
|
||||||
expect(builtInTools).toEqual([]);
|
expect(builtInTools).toEqual([]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
"read",
|
"Read",
|
||||||
"bash",
|
"Bash",
|
||||||
"edit",
|
"Edit",
|
||||||
"write",
|
"Write",
|
||||||
"browser",
|
"browser",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps built-ins as SDK tools when not sandboxed", () => {
|
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.
|
||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: false,
|
sandboxEnabled: false,
|
||||||
});
|
});
|
||||||
expect(builtInTools.map((tool) => tool.name)).toEqual([
|
expect(builtInTools).toEqual([]);
|
||||||
"read",
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
"bash",
|
"Read",
|
||||||
"edit",
|
"Bash",
|
||||||
"write",
|
"Edit",
|
||||||
|
"Write",
|
||||||
|
"browser",
|
||||||
]);
|
]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual(["browser"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -612,7 +612,11 @@ export function createSystemPromptOverride(
|
|||||||
return () => trimmed;
|
return () => trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
|
// 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.
|
||||||
|
// See: https://github.com/anthropics/claude-code/issues/XXX
|
||||||
|
|
||||||
type AnyAgentTool = AgentTool;
|
type AnyAgentTool = AgentTool;
|
||||||
|
|
||||||
@@ -623,19 +627,13 @@ export function splitSdkTools(options: {
|
|||||||
builtInTools: AnyAgentTool[];
|
builtInTools: AnyAgentTool[];
|
||||||
customTools: ReturnType<typeof toToolDefinitions>;
|
customTools: ReturnType<typeof toToolDefinitions>;
|
||||||
} {
|
} {
|
||||||
// SDK rebuilds built-ins from cwd; route sandboxed versions as custom tools.
|
// Always pass all tools as customTools to bypass pi-coding-agent's built-in
|
||||||
const { tools, sandboxEnabled } = options;
|
// tool filtering, which expects lowercase names (bash, read, write, edit).
|
||||||
if (sandboxEnabled) {
|
// Our tools are now capitalized (Bash, Read, Write, Edit) for OAuth compatibility.
|
||||||
return {
|
const { tools } = options;
|
||||||
builtInTools: [],
|
|
||||||
customTools: toToolDefinitions(tools),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
builtInTools: tools.filter((tool) => BUILT_IN_TOOL_NAMES.has(tool.name)),
|
builtInTools: [],
|
||||||
customTools: toToolDefinitions(
|
customTools: toToolDefinitions(tools),
|
||||||
tools.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name)),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
|
|
||||||
it("includes bash and process tools", () => {
|
it("includes bash and process tools", () => {
|
||||||
const tools = createClawdbotCodingTools();
|
const tools = createClawdbotCodingTools();
|
||||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
// 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 === "process")).toBe(true);
|
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,8 +176,9 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(names.has("sessions_send")).toBe(false);
|
expect(names.has("sessions_send")).toBe(false);
|
||||||
expect(names.has("sessions_spawn")).toBe(false);
|
expect(names.has("sessions_spawn")).toBe(false);
|
||||||
|
|
||||||
expect(names.has("read")).toBe(true);
|
// NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking
|
||||||
expect(names.has("bash")).toBe(true);
|
expect(names.has("Read")).toBe(true);
|
||||||
|
expect(names.has("Bash")).toBe(true);
|
||||||
expect(names.has("process")).toBe(true);
|
expect(names.has("process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,18 +190,21 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
agent: {
|
agent: {
|
||||||
subagents: {
|
subagents: {
|
||||||
tools: {
|
tools: {
|
||||||
|
// Policy matching is case-insensitive
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(tools.map((tool) => tool.name)).toEqual(["read"]);
|
// Tool names are capitalized for OAuth compatibility
|
||||||
|
expect(tools.map((tool) => tool.name)).toEqual(["Read"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps read tool image metadata intact", async () => {
|
it("keeps read tool image metadata intact", async () => {
|
||||||
const tools = createClawdbotCodingTools();
|
const tools = createClawdbotCodingTools();
|
||||||
const readTool = tools.find((tool) => tool.name === "read");
|
// NOTE: read is capitalized to bypass Anthropic OAuth blocking
|
||||||
|
const readTool = tools.find((tool) => tool.name === "Read");
|
||||||
expect(readTool).toBeDefined();
|
expect(readTool).toBeDefined();
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
|
||||||
@@ -239,7 +244,8 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
|
|
||||||
it("returns text content without image blocks for text files", async () => {
|
it("returns text content without image blocks for text files", async () => {
|
||||||
const tools = createClawdbotCodingTools();
|
const tools = createClawdbotCodingTools();
|
||||||
const readTool = tools.find((tool) => tool.name === "read");
|
// NOTE: read is capitalized to bypass Anthropic OAuth blocking
|
||||||
|
const readTool = tools.find((tool) => tool.name === "Read");
|
||||||
expect(readTool).toBeDefined();
|
expect(readTool).toBeDefined();
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
|
||||||
@@ -294,8 +300,10 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const tools = createClawdbotCodingTools({ sandbox });
|
const tools = createClawdbotCodingTools({ sandbox });
|
||||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
// NOTE: bash/read are capitalized to bypass Anthropic OAuth blocking
|
||||||
expect(tools.some((tool) => tool.name === "read")).toBe(false);
|
// 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 === "browser")).toBe(false);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -325,16 +333,18 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const tools = createClawdbotCodingTools({ sandbox });
|
const tools = createClawdbotCodingTools({ sandbox });
|
||||||
expect(tools.some((tool) => tool.name === "read")).toBe(true);
|
// NOTE: read/write/edit are capitalized to bypass Anthropic OAuth blocking
|
||||||
expect(tools.some((tool) => tool.name === "write")).toBe(false);
|
expect(tools.some((tool) => tool.name === "Read")).toBe(true);
|
||||||
expect(tools.some((tool) => tool.name === "edit")).toBe(false);
|
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", () => {
|
it("filters tools by agent tool policy even without sandbox", () => {
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
config: { agent: { tools: { deny: ["browser"] } } },
|
config: { agent: { tools: { deny: ["browser"] } } },
|
||||||
});
|
});
|
||||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
// NOTE: bash is capitalized to bypass Anthropic OAuth blocking
|
||||||
|
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
||||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -399,6 +399,29 @@ function normalizeToolNames(list?: string[]) {
|
|||||||
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
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.
|
||||||
|
* @see https://github.com/anthropics/claude-code/issues/XXX
|
||||||
|
*/
|
||||||
|
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 = [
|
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
"sessions_list",
|
"sessions_list",
|
||||||
"sessions_history",
|
"sessions_history",
|
||||||
@@ -724,5 +747,9 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
: sandboxed;
|
: sandboxed;
|
||||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
||||||
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||||
return subagentFiltered.map(normalizeToolParameters);
|
const normalized = subagentFiltered.map(normalizeToolParameters);
|
||||||
|
|
||||||
|
// 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(normalized);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user