fix(gateway): 卸载拒绝话术按接入路径分流(.env 接入的 server 不再指向 MCP 页)
复审 M30-p2-1(r1):`plugin_uninstall_scope` 的 400 一律让主人「去『MCP 服务器』页
卸载」,但 `.env` 的 MCP_SERVERS_JSON 启动期接入的 server **不经**插件管理器、
不在 plugins.json 里,MCP 页(= Manager.Statuses()/List())根本看不到它 ⇒ 那条指引
对该路径是假指引。
- 新增 `plugin.(Manager) MCPServerName`:`mcp:<server>` 注册键 → server 名的**唯一**
解析口(前缀只在 manager.go 出现一次),网关不再自己拼前缀。
- `serverScopedUninstall` 改为 `*Server` 方法,文案经 `uninstallPointer` 分流:
· 管理器托管(落在 plugins.json,判据 `Manager.List()`)⇒ 保留「请在『MCP 服务器』
页启停或卸载该 server」;
· 否则 ⇒ 明说该 server 由 .env 的 MCP_SERVERS_JSON 在启动时接入、面板不管理它,
并给出可操作步骤(改 .env 里那一项:删掉或设 enabled=false,改完需重启生效)。
接入路径判据来自**管理器持久清单**,不是名字前缀/transport 推断。
- `mcp:<server>` 登记条目同样分流(此前也一律指向 MCP 页)。
测试(gateway):plugins.json 路径断言含「MCP 服务器」页字样且不提 MCP_SERVERS_JSON;
.env 路径(工具与登记条目各一条)断言含 MCP_SERVERS_JSON 且**不含**「MCP 服务器」
字样;三条均断言 400 + `plugin_uninstall_scope` 不变、被拒后条目仍在。
修前红:两条 .env 用例(见回报);修后绿。回归 go build/vet/test 全绿。
This commit is contained in:
1 parent
86aef7b454
commit
c608dd4afb
3 files changed
+157
-10
No files matched your search
@@ -139,7 +139,7 @@ func rawEntryOf(t *testing.T, raw []byte, name string) string {
|
||||
}
|
||||
|
||||
// TestAdminPluginUninstallRejectsMCPTool 是那条 high finding 的服务端一半:
|
||||
// 单把 MCP 工具不能绕过插件管理器裸删,必须指回「MCP 服务器」页。
|
||||
// 单把 MCP 工具不能绕过插件管理器裸删(消息该指向哪,按接入路径另有专门用例)。
|
||||
func TestAdminPluginUninstallRejectsMCPTool(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newPluginTestEnv(t, nil)
|
||||
@@ -156,14 +156,111 @@ func TestAdminPluginUninstallRejectsMCPTool(t *testing.T) {
|
||||
if !strings.Contains(string(raw), "plugin_uninstall_scope") {
|
||||
t.Fatalf("错误码不是 plugin_uninstall_scope:%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "MCP 服务器") {
|
||||
t.Fatalf("错误消息没说该去哪里卸:%s", raw)
|
||||
}
|
||||
if _, err := env.caps.Get("read_file_mcp"); err != nil {
|
||||
t.Fatalf("被拒的卸载却把工具删了:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// installMCPPlugin 通过 install 端点装一台 server(写进 plugins.json)。
|
||||
// 用于「这台 server 由插件管理器托管」的路径判定。
|
||||
func installMCPPlugin(t *testing.T, env *pluginTestEnv, name string) {
|
||||
t.Helper()
|
||||
resp, raw := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/admin/plugins/install", map[string]any{
|
||||
"kind": "mcp",
|
||||
"name": name,
|
||||
"config": map[string]any{"transport": "http", "endpoint": env.mcpURL},
|
||||
}, env.token)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("install %q status = %d, body = %s", name, resp.StatusCode, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminPluginUninstallMCPToolPointsToPluginPage:server 在 plugins.json 里
|
||||
// ⇒ 文案指向「MCP 服务器」页(那页确实能卸载它),**不**提 .env。
|
||||
func TestAdminPluginUninstallMCPToolPointsToPluginPage(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newPluginTestEnv(t, newFakeMCPHandler("", "add"))
|
||||
installMCPPlugin(t, env, "calc")
|
||||
|
||||
resp, raw := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/admin/plugins/uninstall",
|
||||
map[string]any{"name": "add"}, env.token)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400, body = %s", resp.StatusCode, raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "plugin_uninstall_scope") {
|
||||
t.Fatalf("错误码不是 plugin_uninstall_scope:%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "MCP 服务器") {
|
||||
t.Fatalf("plugins.json 装出来的 server 应指向「MCP 服务器」页:%s", raw)
|
||||
}
|
||||
if strings.Contains(string(raw), "MCP_SERVERS_JSON") {
|
||||
t.Fatalf("plugins.json 路径不该提 .env 的 MCP_SERVERS_JSON:%s", raw)
|
||||
}
|
||||
if _, err := env.caps.Get("add"); err != nil {
|
||||
t.Fatalf("被拒的卸载却把工具删了:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminPluginUninstallMCPToolPointsToEnvPath:server **不在** plugins.json
|
||||
// ⇒ 它是 `.env` 的 MCP_SERVERS_JSON 启动期接入的,而「MCP 服务器」页只列 manager
|
||||
// 托管的那批(看不见它)⇒ 文案必须让主人去改 .env,且**不得**指向那页。
|
||||
func TestAdminPluginUninstallMCPToolPointsToEnvPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newPluginTestEnv(t, nil)
|
||||
origin := sdk.Origin{Kind: sdk.OriginKindMCP, Server: "envcalc", Transport: "stdio"}
|
||||
if err := env.caps.RegisterTool(originStubTool{name: "env_add", origin: origin}); err != nil {
|
||||
t.Fatalf("register env-path tool: %v", err)
|
||||
}
|
||||
|
||||
resp, raw := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/admin/plugins/uninstall",
|
||||
map[string]any{"name": "env_add"}, env.token)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400, body = %s", resp.StatusCode, raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "plugin_uninstall_scope") {
|
||||
t.Fatalf("错误码不是 plugin_uninstall_scope:%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "MCP_SERVERS_JSON") {
|
||||
t.Fatalf(".env 接入的 server 应指向 MCP_SERVERS_JSON:%s", raw)
|
||||
}
|
||||
if strings.Contains(string(raw), "MCP 服务器") {
|
||||
t.Fatalf(".env 接入的 server 不得指向「MCP 服务器」页(那页看不到它):%s", raw)
|
||||
}
|
||||
if _, err := env.caps.Get("env_add"); err != nil {
|
||||
t.Fatalf("被拒的卸载却把工具删了:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminPluginUninstallMCPEntryPointsToEnvPath:`mcp:<server>` 登记条目同样要按
|
||||
// 接入路径给指引 —— env 路径下也**不得**指向「MCP 服务器」页。
|
||||
func TestAdminPluginUninstallMCPEntryPointsToEnvPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newPluginTestEnv(t, nil)
|
||||
if err := env.caps.Register(capreg.Capability{
|
||||
Name: "mcp:envcalc", Kind: capreg.KindMCP, Description: "MCP server envcalc", Value: struct{}{},
|
||||
}); err != nil {
|
||||
t.Fatalf("register mcp entry: %v", err)
|
||||
}
|
||||
|
||||
resp, raw := doReq(t, http.MethodPost, env.ts.URL+"/api/v1/admin/plugins/uninstall",
|
||||
map[string]any{"name": "mcp:envcalc"}, env.token)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400, body = %s", resp.StatusCode, raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "plugin_uninstall_scope") {
|
||||
t.Fatalf("错误码不是 plugin_uninstall_scope:%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "MCP_SERVERS_JSON") {
|
||||
t.Fatalf(".env 接入的登记条目应指向 MCP_SERVERS_JSON:%s", raw)
|
||||
}
|
||||
if strings.Contains(string(raw), "MCP 服务器") {
|
||||
t.Fatalf(".env 接入的登记条目不得指向「MCP 服务器」页:%s", raw)
|
||||
}
|
||||
if _, err := env.caps.Get("mcp:envcalc"); err != nil {
|
||||
t.Fatalf("被拒的卸载却把登记条目删了:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminPluginUninstallStillRemovesPlainCapability:普通能力(技能/内置工具)
|
||||
// 的卸载路径逐字不变 —— 拒绝只针对「有 MCP 来源」的条目。
|
||||
func TestAdminPluginUninstallStillRemovesPlainCapability(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user