fix(upstream): M51 收敛剩余上游自由文本出口 - CDP/MCP/flowgate/channelbridge WARN
- channelbridge: 拨号失败的 WARN 不再带原始 dial 错误(含 ?access_token=), URLCause 剥壳 + 内嵌地址 MaskQuery 打码 + HostOf 保留目标来源;控制流零改动 - browser/cdp.go: CDP error.message 只有像协议枚举(ErrorToken)才出镜,其余只留字节数 - mcp: 第三方 server 的错误文本默认不带,只留 server/工具/JSON-RPC 码/message 长度, 分类链(errors.Is/As、context.Canceled)经 Unwrap 保留 - channel/adapter/flowgate: 审查输出摘要由 40 字节改 40 rune(防切 UTF-8)并过 RedactText 四处均补了会随回退变红的用例(变异已实测)。
This commit is contained in:
1 parent
2702777950
commit
d6493d79f4
10 files changed
+538
-23
No files matched your search
@@ -0,0 +1,94 @@
|
||||
package mcp
|
||||
|
||||
// 本文件钉住「第三方 MCP server 的错误文本不进错误与日志」这条口径(errors.go)。
|
||||
//
|
||||
// 生产风险:MCP server 是第三方进程/远端服务,它可以把刚收到的工具参数、读到的文件
|
||||
// 内容、甚至自己的系统提示词塞进 error.message。原样经 SDK error 进本包错误,那条
|
||||
// 文本就会随分类链一路进日志、进管理面板、进模型可见视图。
|
||||
//
|
||||
// 变异判据(每条都对应一个会红的实现):
|
||||
// - 把 upstreamError 换成裸 err(或 `%w` 包原始 err)⇒ 哨兵文本出现在错误串里;
|
||||
// - 把 upstreamFailure 的 Error() 改成渲染 cause ⇒ 同上;
|
||||
// - 摘掉 Unwrap ⇒ 「分类仍成立」那条断言红(取消会被当成普通工具失败)。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// errSentinel 是「绝不能出现在错误串里」的第三方自由文本标记。
|
||||
const errSentinel = "MCP-UPSTREAM-SENTINEL-原文回显"
|
||||
|
||||
// sentinelServer 起一台会在 tools/call 里返回带哨兵文本的 JSON-RPC 错误的 server。
|
||||
func sentinelServer() *mcpsdk.Server {
|
||||
srv := mcpsdk.NewServer(&mcpsdk.Implementation{Name: "sentinel-mcp", Version: "1.0.0"}, nil)
|
||||
srv.AddTool(&mcpsdk.Tool{
|
||||
Name: "boom",
|
||||
InputSchema: map[string]any{"type": "object"},
|
||||
}, func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) {
|
||||
return nil, &sdkjsonrpc.Error{Code: sdkjsonrpc.CodeInternalError, Message: errSentinel}
|
||||
})
|
||||
return srv
|
||||
}
|
||||
|
||||
// TestCallTool_KeepsServerErrorTextOut 是 CallTool 的正面证据:错误里只剩
|
||||
// server / 工具 / 协议码 / 长度,哨兵文本一个字节都没有。
|
||||
func TestCallTool_KeepsServerErrorTextOut(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := connectFakeClient(t, sentinelServer())
|
||||
_, err := c.CallTool(context.Background(), "boom", map[string]any{})
|
||||
if err == nil {
|
||||
t.Fatal("server 返回协议错误时 CallTool 应当报错")
|
||||
}
|
||||
if strings.Contains(err.Error(), errSentinel) {
|
||||
t.Fatalf("第三方 server 的自由文本进了错误串:%v", err)
|
||||
}
|
||||
for _, want := range []string{"boom", "fake", "code=-32603"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("错误里应保留可分类的骨架 %q:%v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpstreamError_KeepsClassificationButNotText 直接钉住 errors.go 的契约:
|
||||
// Error() 只有骨架,errors.Is/As 仍然看得见原始原因。
|
||||
func TestUpstreamError_KeepsClassificationButNotText(t *testing.T) {
|
||||
t.Parallel()
|
||||
cause := fmt.Errorf("sdk wrapper: %w", &sdkjsonrpc.Error{Code: -32601, Message: errSentinel})
|
||||
err := upstreamError(cause)
|
||||
|
||||
if strings.Contains(err.Error(), errSentinel) {
|
||||
t.Fatalf("自由文本进了错误串:%v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "code=-32601") {
|
||||
t.Errorf("错误里丢了 JSON-RPC 码:%v", err)
|
||||
}
|
||||
// 分类面:WireError 的 Is 比的是码,链不能断。
|
||||
if !errors.Is(err, &sdkjsonrpc.Error{Code: -32601}) {
|
||||
t.Errorf("errors.Is 丢失了 JSON-RPC 码分类:%v", err)
|
||||
}
|
||||
var wire *sdkjsonrpc.Error
|
||||
if !errors.As(err, &wire) || wire.Code != -32601 {
|
||||
t.Errorf("errors.As 取不到原始 JSON-RPC 错误:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpstreamError_KeepsContextCancellation 钉住「取消仍可分类」:上层靠
|
||||
// errors.Is(err, context.Canceled) 区分「用户取消」与「工具失败」,
|
||||
// 收敛文本时绝不能把这条链切断。
|
||||
func TestUpstreamError_KeepsContextCancellation(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := upstreamError(fmt.Errorf("mcp: %w", context.Canceled))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("errors.Is(err, context.Canceled) = false,取消被吞掉了:%v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
t.Errorf("取消的原文也被带进了错误串:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user