fix: restore Anthropic OAuth tool dispatch
This commit is contained in:
@@ -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" }),
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user