-
Notifications
You must be signed in to change notification settings - Fork 159
fix: obfuscate tool names to bypass API blacklist detection #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
41bbb62
6551d14
7053aa3
9420fbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,27 @@ | ||
| import { buildBillingHeaderValue } from "./signing.ts" | ||
| import { config, getModelOverride } from "./model-config.ts" | ||
|
|
||
| const TOOL_PREFIX = "mcp_" | ||
| // Obfuscate tool names: the API blacklists certain tool names (todowrite, | ||
| // background_output, background_cancel). To avoid detection we hash ALL tool | ||
| // names on the way out and reverse-map them on the way back. | ||
| import { createHash } from "node:crypto" | ||
|
|
||
| const toolNameMap = new Map<string, string>() // obfuscated → original | ||
| const toolNameReverseMap = new Map<string, string>() // original → obfuscated | ||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/transforms.ts
Line: 9-10
Comment:
**Module-level maps are never cleared and silently fail on deobfuscation miss**
`toolNameMap` and `toolNameReverseMap` live for the lifetime of the process and are never reset. In the current single-process model that is fine, but `deobfuscateToolName` returns the raw hash when a key is absent (the `?? obf` fallback). If that ever triggers — e.g., during a hot-reload in development, or if a response somehow references a tool that was registered in a prior process lifetime — OpenCode receives obfuscated names like `t_a1b2c3d4` instead of original ones and silently misroutes tool calls. A warning log or a thrown error on a miss would make this failure mode visible.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| function obfuscateToolName(name: string): string { | ||
| const existing = toolNameReverseMap.get(name) | ||
| if (existing) return existing | ||
| const hash = createHash("md5").update(name).digest("hex").slice(0, 8) | ||
| const obf = `t_${hash}` | ||
| toolNameMap.set(obf, name) | ||
| toolNameReverseMap.set(name, obf) | ||
| return obf | ||
| } | ||
|
|
||
| function deobfuscateToolName(obf: string): string { | ||
| return toolNameMap.get(obf) ?? obf | ||
| } | ||
|
|
||
| const SYSTEM_IDENTITY = | ||
| "You are Claude Code, Anthropic's official CLI for Claude." | ||
|
|
@@ -206,30 +226,30 @@ export function transformBody( | |
| } | ||
| } | ||
|
|
||
| // Obfuscate tool names to avoid API blacklist detection | ||
| if (Array.isArray(parsed.tools)) { | ||
| parsed.tools = parsed.tools.map((tool) => ({ | ||
| ...tool, | ||
| name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name, | ||
| name: tool.name ? obfuscateToolName(tool.name) : tool.name, | ||
| })) | ||
| } | ||
|
|
||
| if (Array.isArray(parsed.messages)) { | ||
| parsed.messages = parsed.messages.map((message) => { | ||
| if (!Array.isArray(message.content)) { | ||
| return message | ||
| } | ||
|
|
||
| if (!Array.isArray(message.content)) return message | ||
| return { | ||
| ...message, | ||
| content: message.content.map((block) => { | ||
| if (block.type !== "tool_use" || typeof block.name !== "string") { | ||
| return block | ||
| if (block.type === "tool_use" && typeof block.name === "string") { | ||
| return { ...block, name: obfuscateToolName(block.name) } | ||
| } | ||
|
|
||
| return { | ||
| ...block, | ||
| name: `${TOOL_PREFIX}${block.name}`, | ||
| if ( | ||
| block.type === "tool_result" && | ||
| typeof block["tool_use_id"] === "string" | ||
| ) { | ||
| // tool_result references tool_use by id, not name — no change needed | ||
| } | ||
|
Comment on lines
+246
to
251
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The branch checks Prompt To Fix With AIThis is a comment left during a code review.
Path: src/transforms.ts
Line: 246-251
Comment:
**Dead `tool_result` if-branch adds noise without effect**
The branch checks `block.type === "tool_result"` then does nothing — execution falls straight through to `return block` in both cases. The comment is useful but wrapping it in an inert `if` block suggests an action that never happens, which makes the control flow misleading for future readers. The block should either be removed or kept as a plain comment outside any conditional.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| return block | ||
| }), | ||
| } | ||
| }) | ||
|
|
@@ -246,7 +266,11 @@ export function transformBody( | |
| } | ||
|
|
||
| export function stripToolPrefix(text: string): string { | ||
| return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') | ||
| // Reverse-map obfuscated tool names back to originals in response stream | ||
| return text.replace(/"name"\s*:\s*"(t_[0-9a-f]{8})"/g, (_match, obf) => { | ||
| const original = deobfuscateToolName(obf) | ||
| return `"name": "${original}"` | ||
| }) | ||
| } | ||
|
|
||
| export function transformResponseStream(response: Response): Response { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
splitAt = 15does not split inside the tool namedata: {"name":"is exactly 15 characters, sochunk1ends with the opening quote andchunk2starts with the fullt_XXXXXXXXtoken — the split lands just before the name, not inside it. The original test deliberately putmcin chunk1 andp_search"}\n\n…in chunk2, proving the buffer handles a name torn mid-token. UsingsplitAt = 17or19(somewhere insidet_XXXXXXXX) would restore that property and make the comment accurate.Prompt To Fix With AI