From 6004ebdfdb0f9870f5cdfbf765f5dfaadd1d9242 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 30 Sep 2026 12:04:58 +0800 Subject: [PATCH] fix(claude): fail open on unmapped oauth mcp tool aliases - Forward unmapped Claude OAuth MCP tool aliases unchanged instead of returning an error - Log a warning when no request-local tool match is found to prevent stream termination Closes: #6208 --- .../executor/claude_executor_request.go | 3 +- .../claude_executor_request_remap_test.go | 89 +++++++++++++++---- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/internal/runtime/executor/claude_executor_request.go b/internal/runtime/executor/claude_executor_request.go index 43c3755e8..054f13770 100644 --- a/internal/runtime/executor/claude_executor_request.go +++ b/internal/runtime/executor/claude_executor_request.go @@ -2476,7 +2476,8 @@ func (resolver claudeMCPAliasResolver) resolve(name string) (string, bool, error } } - return "", false, claudeMCPAliasRestoreError{fmt.Errorf("cannot restore Claude OAuth MCP tool alias %q: no unique request-local match", name)} + log.Warnf("claude oauth mcp alias: cannot restore tool name %q: no unique request-local match; forwarding it unchanged", name) + return "", false, nil } // reverseRemapOAuthToolNames reverses the tool name mapping for non-stream responses diff --git a/internal/runtime/executor/claude_executor_request_remap_test.go b/internal/runtime/executor/claude_executor_request_remap_test.go index 138f1669e..13e071b58 100644 --- a/internal/runtime/executor/claude_executor_request_remap_test.go +++ b/internal/runtime/executor/claude_executor_request_remap_test.go @@ -340,11 +340,6 @@ func TestReverseRemapOAuthToolNamesRejectsUnsafeMangledAliases(t *testing.T) { alias: "mcp__" + firstParts.server + "__" + unknownToolID[:len(unknownToolID)-1] + "_" + firstParts.semantic, wantError: "semantic suffix matches multiple declared tools", }, - { - name: "unrecoverable semantic suffix", - alias: "mcp__" + firstParts.server + "__" + unknownToolID + "_missing_tool", - wantError: "no unique request-local match", - }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -369,6 +364,27 @@ func TestReverseRemapOAuthToolNamesRejectsUnsafeMangledAliases(t *testing.T) { } }) } + + t.Run("unrecoverable semantic suffix fails open", func(t *testing.T) { + unrecoverableAlias := "mcp__" + firstParts.server + "__" + unknownToolID + "_missing_tool" + response := []byte(fmt.Sprintf(`{"content":[{"type":"tool_use","id":"toolu_1","name":%q,"input":{}}]}`, unrecoverableAlias)) + restored, errReverse := reverseRemapOAuthToolNames(response, reverseMap) + if errReverse != nil { + t.Fatalf("reverseRemapOAuthToolNames() error = %v, want fail-open nil", errReverse) + } + if got := gjson.GetBytes(restored, "content.0.name").String(); got != unrecoverableAlias { + t.Fatalf("unrecoverable alias = %q, want %q forwarded unchanged", got, unrecoverableAlias) + } + + line := []byte(fmt.Sprintf(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_1","name":%q,"input":{}}}`, unrecoverableAlias)) + restoredLine, errStream := reverseRemapOAuthToolNamesFromStreamLine(line, reverseMap) + if errStream != nil { + t.Fatalf("reverseRemapOAuthToolNamesFromStreamLine() error = %v, want fail-open nil", errStream) + } + if !bytes.Equal(restoredLine, line) { + t.Fatalf("stream line = %s, want it forwarded unchanged", restoredLine) + } + }) } func TestReverseRemapOAuthToolNames_OverlappingSemanticSuffix(t *testing.T) { @@ -560,35 +576,37 @@ func TestRemapKeepsReverseMapEmptyWhenOnlyCallerMCPToolsArePresent(t *testing.T) } } -func TestReverseRemapOAuthToolNamesMarksTrailingMarkupFailureRequestScoped(t *testing.T) { +// TestReverseRemapOAuthToolNamesFailsOpenOnTrailingMarkup verifies that an alias with +// trailing prompt markup glued to it is forwarded verbatim instead of failing the +// response or terminating the SSE stream. +func TestReverseRemapOAuthToolNamesFailsOpenOnTrailingMarkup(t *testing.T) { const alias = "mcp__hmzqrngkulqv__xuo7jlxlpzee_clear_thinking" malformedAlias := alias + "\n