fix(claude): apply tool cloak + schema sanitize on the CLIProxyAPI executor path

The native Claude OAuth guard in executors/base.ts is bypassed when
`upstream_proxy_config.mode = cliproxyapi` routes the request through the
CliproxyAPI executor — it has its own execute()/transformRequest() and never
reaches BaseExecutor.execute(), so the cloak/sanitizer never ran for that
(common) deployment. Wire the same guards into
CliproxyapiExecutor.transformRequest (Anthropic-shape branch), composing with
the existing bisected `mcp_*` reserved-namespace rewrite:

- sanitizeClaudeToolSchemas() on transformed.tools.
- cloakThirdPartyToolNames() with skip = mcp-reserved, so applyMcpToolNameRewrite
  keeps authority over `mcp_*` (its bisected `Mcp_X` form) and the two reverse
  maps stay disjoint / single-hop. Both merge into the non-enumerable
  _toolNameMap the response stream already uses to restore the caller's names.

cloakThirdPartyToolNames is now non-mutating (clones changed entries) to respect
transformRequest's no-input-mutation contract, and takes an optional `skip`
predicate.

Verified end-to-end through the live CPA path: a real ~100-tool harness payload
that returned the "out of extra usage" placeholder now returns 200 with original
tool names restored on the response stream; `mcp_*` tools and genuine PascalCase
Claude Code tools are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
NomenAK
2026-05-30 13:59:36 +00:00
parent 3b2d075402
commit 23dad7d93d
3 changed files with 102 additions and 16 deletions

View File

@@ -179,3 +179,31 @@ describe("cloakThirdPartyToolNames — defensive null guards", () => {
assert.equal(block.name, "Read");
});
});
describe("cloakThirdPartyToolNames — non-mutating + skip option", () => {
it("does not mutate the caller's input tool objects", () => {
const original: AnyRecord = { name: "read_file" };
const body: AnyRecord = { tools: [original] };
cloakThirdPartyToolNames(body);
assert.equal(original.name, "read_file"); // input object untouched
assert.equal((body.tools as AnyRecord[])[0].name, "Read"); // body.tools reassigned with a clone
});
it("does not mutate the caller's input message blocks", () => {
const block: AnyRecord = { type: "tool_use", name: "read_file" };
const body: AnyRecord = {
tools: [{ name: "read_file" }],
messages: [{ role: "assistant", content: [block] }],
};
cloakThirdPartyToolNames(body);
assert.equal(block.name, "read_file"); // input block untouched
const out = ((body.messages as AnyRecord[])[0].content as AnyRecord[])[0];
assert.equal(out.name, "Read");
});
it("leaves names matched by the skip predicate untouched", () => {
const body: AnyRecord = { tools: [{ name: "mcp_call" }, { name: "read_file" }] };
cloakThirdPartyToolNames(body, { skip: (n) => n.startsWith("mcp_") });
assert.deepEqual((body.tools as AnyRecord[]).map((t) => t.name), ["mcp_call", "Read"]);
});
});