mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户需求:插件可安装、运行时修改适配器以支持复杂认证与 token 轮换;
适配器本身不需要持续运行(它是无状态请求级弹性池)。
插件(长期运行)─定期/事件─> install/update/remove_adapter
│
▼
内核重建该适配器的池(新池先发布,旧池后回收)
│
▼
后续请求自然使用新逻辑
三条安全规则(与 R6 同源):
1. 路径安全:ValidateAdapterName(无穿越/绝对路径/空白)
2. 不得影子内置:插件不能注册与内置同名适配器(运维仍可自行覆盖)
3. 归属可查:owner 表记录每个适配器由谁安装,于是"插件只能改自己装的"
是可判定的,而非靠约定。没有归属信息时,插件 A 能劫持插件 B 的认证逻辑
而无法被察觉。
修掉两个开发中暴露的真实缺陷:
1. 池替换窗口:LoadAdapterSource 原先在 v.mu 内先 shutdown 旧池再装新池,
窗口期内请求拿到 "pool closed"。改为**新池先发布、旧池后回收**。
2. 并发轮换仍失败:多个 goroutine 同时轮换时,单次重试会连续落在刚被下个
轮换关闭的池上。acquireAcrossSwap 改为按**墙钟**(2s 上限)+1ms 退避
重试新池 —— 轮换是运维的意图,落在微秒窗口里的请求不该因此失败。
上限保证适配器真消失时仍会及时报错而非空转。
3. 声明名 vs 注册键:脚本可声明与注册键不同的 name(插件按自己控制的键注册,
但适配器声明源真正引用的名字)。归属表同时记两个键,删除时一并清理;
EffectiveAdapterName 在声明名下也能查到池。
测试:11 个(安装/轮换/影子内置/跨插件窃取/碰运维适配器/路径穿越/删除/
归属列举/并发 8×20 压力/语法错误安装即拒/声明名上报)。
注:internal/gateway 的变异测试 harness 拒绝在脏工作区运行(设计如此),
故本提交后它才可运行。
265 lines
8.5 KiB
Go
265 lines
8.5 KiB
Go
package lua
|
||
|
||
import (
|
||
"encoding/json"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
)
|
||
|
||
func bridgeVM(t *testing.T) (*VM, *Plugins) {
|
||
t.Helper()
|
||
dir := filepath.Join(t.TempDir(), "adapters")
|
||
vm := NewVM(dir)
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(vm.Stop)
|
||
ps := NewPlugins(vm, filepath.Join(t.TempDir(), "plugins"))
|
||
return vm, ps
|
||
}
|
||
|
||
const tokenAdapterV1 = `
|
||
local adapter = {}
|
||
adapter.name = "myauth"
|
||
adapter.version = "1.0"
|
||
adapter.endpoint = "/chat/completions"
|
||
function adapter.transform_request(body)
|
||
return '{"token":"TOKEN_V1"}'
|
||
end
|
||
return adapter
|
||
`
|
||
|
||
const tokenAdapterV2 = `
|
||
local adapter = {}
|
||
adapter.name = "myauth"
|
||
adapter.version = "2.0"
|
||
adapter.endpoint = "/chat/completions"
|
||
function adapter.transform_request(body)
|
||
return '{"token":"TOKEN_V2"}'
|
||
end
|
||
return adapter
|
||
`
|
||
|
||
// TestPluginInstallsAdapter 是基础能力:插件装的适配器立即可用。
|
||
func TestPluginInstallsAdapter(t *testing.T) {
|
||
vm, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
|
||
t.Fatalf("install: %v", err)
|
||
}
|
||
if !vm.HasFn("myauth", "transform_request") {
|
||
t.Fatal("安装后适配器未注册")
|
||
}
|
||
out, err := vm.Transform("myauth", "transform_request", "{}")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(out, "TOKEN_V1") {
|
||
t.Errorf("安装的适配器未被使用: %s", out)
|
||
}
|
||
}
|
||
|
||
// TestPluginUpdateAdapterRotatesToken 是核心用例:**token 轮换**。
|
||
// 插件长期运行,在运行中改写适配器,后续请求用新 token —— 适配器自身
|
||
// 不跑任何循环。
|
||
func TestPluginUpdateAdapterRotatesToken(t *testing.T) {
|
||
vm, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
first, _ := vm.Transform("myauth", "transform_request", "{}")
|
||
if !strings.Contains(first, "TOKEN_V1") {
|
||
t.Fatalf("初次应带 V1: %s", first)
|
||
}
|
||
|
||
// 轮换:插件改写适配器
|
||
if err := ps.UpdateAdapter("authtoken", "myauth", tokenAdapterV2); err != nil {
|
||
t.Fatalf("update: %v", err)
|
||
}
|
||
second, err := vm.Transform("myauth", "transform_request", "{}")
|
||
if err != nil {
|
||
t.Fatalf("改写后适配器不可用: %v", err)
|
||
}
|
||
if !strings.Contains(second, "TOKEN_V2") {
|
||
t.Errorf("轮换后应带 V2: %s", second)
|
||
}
|
||
if strings.Contains(second, "TOKEN_V1") {
|
||
t.Errorf("轮换后仍带旧 V1(池未重建?): %s", second)
|
||
}
|
||
// 版本号也应更新,便于运维从 /api/status 看出发生过轮换
|
||
if v := vm.AdapterVersion("myauth"); v != "2.0" {
|
||
t.Errorf("适配器版本 = %q, want 2.0", v)
|
||
}
|
||
}
|
||
|
||
// TestPluginCannotShadowBuiltin:插件不能篡改内核自带的适配器。
|
||
func TestPluginCannotShadowBuiltin(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
evil := `local a={} a.name="openai" a.version="EVIL" a.endpoint="/x" return a`
|
||
if _, err := ps.InstallAdapter("sneaky", "openai", evil); err == nil {
|
||
t.Fatal("插件装与内置同名的适配器应被拒")
|
||
}
|
||
}
|
||
|
||
// TestPluginCannotStealAnotherPluginsAdapter:归属隔离。
|
||
func TestPluginCannotStealAnotherPluginsAdapter(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("alpha", "shared_auth", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ps.InstallAdapter("beta", "shared_auth", tokenAdapterV2); err == nil {
|
||
t.Error("插件 B 应无法覆盖插件 A 装的适配器")
|
||
}
|
||
if err := ps.UpdateAdapter("beta", "shared_auth", tokenAdapterV2); err == nil {
|
||
t.Error("插件 B 应无法更新插件 A 的适配器")
|
||
}
|
||
if err := ps.RemoveAdapter("beta", "shared_auth"); err == nil {
|
||
t.Error("插件 B 应无法删除插件 A 的适配器")
|
||
}
|
||
// 属主自己可以
|
||
if err := ps.UpdateAdapter("alpha", "shared_auth", tokenAdapterV2); err != nil {
|
||
t.Errorf("属主应能更新自己的适配器: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestPluginCannotTouchOperatorAdapter:运维的适配器不可被插件改写。
|
||
func TestPluginCannotTouchOperatorAdapter(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
if err := ps.UpdateAdapter("authtoken", "openai", tokenAdapterV1); err == nil {
|
||
t.Error("插件更新运维适配器应被拒")
|
||
}
|
||
if err := ps.RemoveAdapter("authtoken", "openai"); err == nil {
|
||
t.Error("插件删除运维适配器应被拒")
|
||
}
|
||
}
|
||
|
||
// TestPluginAdapterNameTraversal:路径穿越名在桥接层同样被拒。
|
||
func TestPluginAdapterNameTraversal(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
for _, bad := range []string{"../evil", "/etc/passwd", "has space", ".."} {
|
||
if _, err := ps.InstallAdapter("authtoken", bad, tokenAdapterV1); err == nil {
|
||
t.Errorf("桥接层应拒绝路径穿越名 %q", bad)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestPluginRemoveAdapter 让插件能回收自己的适配器(token 失效/功能下线)。
|
||
func TestPluginRemoveAdapter(t *testing.T) {
|
||
vm, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.RemoveAdapter("authtoken", "myauth"); err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
if vm.HasFn("myauth", "transform_request") {
|
||
t.Error("删除后适配器仍在")
|
||
}
|
||
if owners := ps.PluginAdapters("authtoken"); len(owners) != 0 {
|
||
t.Errorf("删除后归属表应清空,实得 %v", owners)
|
||
}
|
||
}
|
||
|
||
// TestPluginAdapterOwnershipListing 供 UI/诊断用。
|
||
func TestPluginAdapterOwnershipListing(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("alpha", "a1", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ps.InstallAdapter("alpha", "a2", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ps.InstallAdapter("beta", "b1", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := ps.PluginAdapters("alpha")
|
||
if len(got) != 2 || got[0] != "a1" || got[1] != "a2" {
|
||
t.Errorf("alpha 的适配器 = %v, want [a1 a2]", got)
|
||
}
|
||
if o := ps.AdapterOwnerOf("b1"); o != "beta" {
|
||
t.Errorf("b1 属主 = %q, want beta", o)
|
||
}
|
||
if o := ps.AdapterOwnerOf("openai"); o != "" {
|
||
t.Errorf("内置适配器不应有插件属主,实得 %q", o)
|
||
}
|
||
}
|
||
|
||
// TestConcurrentPluginAdapterUpdates 是压力测试:多 goroutine 轮换同一适配器。
|
||
// 池重建与归属表都要在并发下自洽(不 panic、不串属主)。
|
||
func TestConcurrentPluginAdapterUpdates(t *testing.T) {
|
||
vm, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const workers, iters = 8, 20
|
||
var wg sync.WaitGroup
|
||
errs := make(chan string, workers*iters)
|
||
for w := 0; w < workers; w++ {
|
||
wg.Add(1)
|
||
go func(w int) {
|
||
defer wg.Done()
|
||
for i := 0; i < iters; i++ {
|
||
code := tokenAdapterV1
|
||
if i%2 == 1 {
|
||
code = tokenAdapterV2
|
||
}
|
||
if err := ps.UpdateAdapter("authtoken", "myauth", code); err != nil {
|
||
errs <- "update: " + err.Error()
|
||
return
|
||
}
|
||
// 更新后必须仍可用(池重建期间不能出现「找不到适配器」)
|
||
if _, err := vm.Transform("myauth", "transform_request", "{}"); err != nil {
|
||
errs <- "update 后适配器不可用: " + err.Error()
|
||
return
|
||
}
|
||
}
|
||
}(w)
|
||
}
|
||
wg.Wait()
|
||
close(errs)
|
||
for e := range errs {
|
||
t.Error(e)
|
||
}
|
||
if o := ps.AdapterOwnerOf("myauth"); o != "authtoken" {
|
||
t.Errorf("并发后属主应为 authtoken,实得 %q", o)
|
||
}
|
||
}
|
||
|
||
// TestInstalledAdapterBodyIsValidJSON 确认插件提供的代码必须真能编译成
|
||
// 可用适配器 —— 语法错误在安装时就报,而不是等到第一个请求。
|
||
func TestInstalledAdapterBodyIsValidJSON(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
if _, err := ps.InstallAdapter("bad", "broken", "this is not lua ==="); err == nil {
|
||
t.Error("语法错误的适配器应在安装时被拒")
|
||
}
|
||
if _, err := ps.InstallAdapter("bad", "notatable", "return 42"); err == nil {
|
||
t.Error("未返回 table 的适配器应在安装时被拒")
|
||
}
|
||
// 失败的安装不应留下归属记录
|
||
if o := ps.AdapterOwnerOf("broken"); o != "" {
|
||
t.Errorf("安装失败却留下了属主记录: %q", o)
|
||
}
|
||
if o := ps.AdapterOwnerOf("notatable"); o != "" {
|
||
t.Errorf("安装失败却留下了属主记录: %q", o)
|
||
}
|
||
}
|
||
|
||
// TestInstallAdapterReportsActualName 适配器脚本可声明与注册键不同的 name;
|
||
// 调用方拿到实际名,便于更新配置。
|
||
func TestInstallAdapterReportsActualName(t *testing.T) {
|
||
_, ps := bridgeVM(t)
|
||
code := `local a={} a.name="declared_name" a.version="1" a.endpoint="/x" return a`
|
||
got, err := ps.InstallAdapter("p", "registration_key", code)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "declared_name" {
|
||
t.Errorf("返回名 = %q, want declared_name", got)
|
||
}
|
||
if o := ps.AdapterOwnerOf("declared_name"); o != "p" {
|
||
t.Errorf("归属应记在声明名上,实得 %q", o)
|
||
}
|
||
var _ = json.Marshal
|
||
}
|