package tool import ( "encoding/json" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本文件钉住「模型可见的工具面**零影响**」(DevDocs/audit/MCP工具按上游分组-2026-10-24.md // §2 方案①的影响表):来源章只是管理面的呈现归属,绝不进发给模型的函数签名。 // // 判据是**泄漏**:工具面上既不能出现 origin 这个键,也不能出现 server 名 —— // 一旦有人把归属顺手塞进 ToolSpec("让模型知道工具从哪来"),这条就红。 // originMockTool 是带来源章的工具(包一层既有 mockTool,只为提供 Origin)。 type originMockTool struct { mockTool origin sdk.Origin } func (m originMockTool) Origin() sdk.Origin { return m.origin } var _ sdk.OriginAware = originMockTool{} // TestToolSchemasDoNotLeakOrigin:注册一把来自 "fs-origin-probe" 的 MCP 工具, // 发给模型的 schema 里既没有来源键、也没有 server 名。 func TestToolSchemasDoNotLeakOrigin(t *testing.T) { d, reg := newDispatcher(t) probe := originMockTool{ mockTool: mockTool{ name: "read_file", desc: "读文件", params: map[string]any{"type": "object"}, }, origin: sdk.Origin{Kind: sdk.OriginKindMCP, Server: "fs-origin-probe", Transport: "stdio"}, } if err := reg.RegisterTool(probe); err != nil { t.Fatalf("RegisterTool: %v", err) } specs := d.ToolSchemas() if len(specs) != 1 || specs[0].Name != "read_file" { t.Fatalf("ToolSchemas = %+v, want 仅 read_file", specs) } raw, err := json.Marshal(specs) if err != nil { t.Fatalf("marshal schemas: %v", err) } wire := string(raw) if strings.Contains(wire, "origin") { t.Fatalf("模型可见的工具面带上了来源键:%s(来源只用于管理面呈现)", wire) } if strings.Contains(wire, "fs-origin-probe") { t.Fatalf("模型可见的工具面泄漏了 server 名:%s", wire) } // 来源章仍在管理面上(注册表条目),两者不是同一份事实。 cap, gerr := reg.Get("read_file") if gerr != nil { t.Fatalf("Get: %v", gerr) } if cap.Origin.Server != "fs-origin-probe" { t.Fatalf("管理面上的来源章丢了:%+v", cap.Origin) } }