package lua import ( "encoding/json" "os" "path/filepath" "testing" ) // 持久化的出站主机授权必须在**重启后自动生效**。 // // 实测缺陷(生产):LoadKernelPermissions 把已批准的 host 装进 ps.extraHosts, // 但注释明说"折叠由调用方负责" —— 启动路径上没人负责。SetOutboundHosts 早已 // 跑过(core.New 里、LoadDir 之前),于是 allowlist 只有 source 主机, // 插件声明的整套 host(zcode.z.ai / copilot.tencent.com …)在重启后全被拒: // // host "zcode.z.ai" is not an allowed outbound destination // // 症状是经典的"重启丢掉运维的决定"——运维一打开插件页重新保存就恢复。 // 它还打坏了定时任务:refresh_models 在启动后 5s 触发,正好落在那个窗口里, // 于是每个平台的刷新都报"授权缺失"。 func TestPersistedHostGrantsSurviveRestart(t *testing.T) { dir := t.TempDir() plugDir := filepath.Join(dir, "plugins") if err := os.MkdirAll(plugDir, 0o755); err != nil { t.Fatal(err) } // 造一份持久化授权(模拟运维此前在 WebUI 批准过) perms := map[string]interface{}{ "channels": map[string]interface{}{ "actions": []string{"add_source"}, "hosts": []string{"zcode.z.ai", "copilot.tencent.com"}, }, } b, _ := json.Marshal(perms) if err := os.WriteFile(filepath.Join(plugDir, ".kernel-perms.json"), b, 0o600); err != nil { t.Fatal(err) } // 模拟启动:先 SetOutboundHosts(源主机),再 LoadDir(读授权) vm := NewVM(filepath.Join(dir, "adapters")) if err := vm.Start(); err != nil { t.Fatal(err) } t.Cleanup(vm.Stop) ps := NewPlugins(vm, plugDir) ps.SetOutboundHosts([]string{"api.example.com"}) // LoadDir 会读授权文件;没有插件文件也没关系,关键是它必须折叠授权。 if err := ps.LoadDir(); err != nil { t.Fatalf("LoadDir: %v", err) } al := ps.outboundAllowlist() if al == nil { t.Fatal("无 allowlist") } // ① 源主机仍可用 if !al.allows("api.example.com", "443") { t.Error("source 主机被丢了") } // ② 已批准的 extra host 必须生效(这就是本判据的核心) for _, h := range []string{"zcode.z.ai", "copilot.tencent.com"} { if !al.allows(h, "443") { t.Errorf("重启后已批准的 host %q 被拒 —— "+ "LoadKernelPermissions 装了 extraHosts 但没人折叠进 allowlist", h) } } // ③ 没批准过的仍然不许 if al.allows("evil.example.com", "443") { t.Error("未批准的 host 不该被放行") } }