mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(lua): 插件适配器管理桥接(R4-a)—— 安装/改写/删除,支撑 token 轮换
用户需求:插件可安装、运行时修改适配器以支持复杂认证与 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 拒绝在脏工作区运行(设计如此),
故本提交后它才可运行。
This commit is contained in:
187
internal/lua/plugin_adapter_bridge.go
Normal file
187
internal/lua/plugin_adapter_bridge.go
Normal file
@ -0,0 +1,187 @@
|
||||
// Package lua — 插件适配器管理桥接(R4-a / Phase 3)
|
||||
//
|
||||
// 用户需求:插件可安装、运行时修改适配器,以支持复杂认证与 token 轮换;
|
||||
// 适配器本身不需要持续运行(它是无状态请求级弹性池)。
|
||||
//
|
||||
// 插件(长期运行)──定期/事件驱动──> install/update/remove_adapter
|
||||
// │
|
||||
// ▼
|
||||
// 内核重建该适配器的池(LoadAdapterSource)
|
||||
// │
|
||||
// ▼
|
||||
// 后续请求自然使用新逻辑
|
||||
//
|
||||
// 三条安全规则(与 R6 同源):
|
||||
//
|
||||
// 1. 路径安全:名字必须通过 ValidateAdapterName(无穿越/绝对路径/空白)
|
||||
// 2. 不得影子内置:插件不能注册与内置适配器同名的适配器
|
||||
// (用 VM.InstallPluginAdapter,运维仍可自行覆盖)
|
||||
// 3. 归属可查:记录每个适配器的 owner("plugin:名字" 或 "operator"),
|
||||
// 于是「插件只能改自己装的」这条规则是可判定的,而不是靠约定
|
||||
//
|
||||
// 第 3 条是必要的:没有归属信息时,"插件 A 改插件 B 装的适配器" 无法区分
|
||||
// 于合法的自身更新,会导致一个插件能劫持另一个插件的认证逻辑。
|
||||
package lua
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// AdapterOwner 标识一个适配器由谁安装。
|
||||
const (
|
||||
OwnerOperator = "operator" // 磁盘/首启 seed/管理员上传
|
||||
OwnerPlugin = "plugin" // 由某个插件经桥接安装
|
||||
)
|
||||
|
||||
// adapterOwnership records which plugin installed each adapter.
|
||||
//
|
||||
// This is the state that makes rule 3 enforceable: InstallPluginAdapter marks
|
||||
// the adapter as owned by that plugin, and a later update from a DIFFERENT
|
||||
// plugin is refused. An adapter with no owner is the operator's, and no plugin
|
||||
// may touch it.
|
||||
type adapterOwnership struct {
|
||||
mu sync.RWMutex
|
||||
// owner maps adapter name -> installing plugin name (only for plugin-owned).
|
||||
owner map[string]string
|
||||
}
|
||||
|
||||
// ownedBy reports the plugin that owns name, or "" when it is operator-owned.
|
||||
func (o *adapterOwnership) ownedBy(name string) string {
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
return o.owner[name]
|
||||
}
|
||||
|
||||
func (o *adapterOwnership) set(name, plugin string) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
if o.owner == nil {
|
||||
o.owner = map[string]string{}
|
||||
}
|
||||
o.owner[name] = plugin
|
||||
}
|
||||
|
||||
func (o *adapterOwnership) clear(name string) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
delete(o.owner, name)
|
||||
}
|
||||
|
||||
// listOwned returns the adapter names a given plugin owns (for UI/diagnostics).
|
||||
func (o *adapterOwnership) listOwned(plugin string) []string {
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
var out []string
|
||||
for name, p := range o.owner {
|
||||
if p == plugin {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Plugins carries an adapter-ownership table.
|
||||
func (ps *Plugins) adapterOwner() *adapterOwnership {
|
||||
ps.adapterOwnOnce.Do(func() { ps.adapterOwn = &adapterOwnership{} })
|
||||
return ps.adapterOwn
|
||||
}
|
||||
|
||||
// InstallAdapter registers an adapter on behalf of a plugin.
|
||||
//
|
||||
// Returns the name that was actually registered — an adapter script may declare
|
||||
// a different `name` than the file/registration key, and the declared name is
|
||||
// what sources must reference. Callers get it back so they can update config.
|
||||
func (ps *Plugins) InstallAdapter(plugin, name, code string) (string, error) {
|
||||
if plugin == "" {
|
||||
return "", fmt.Errorf("install_adapter: caller plugin is unknown")
|
||||
}
|
||||
if ps.vm == nil {
|
||||
return "", fmt.Errorf("install_adapter: no adapter VM available")
|
||||
}
|
||||
// Rule 3 first: an adapter another plugin already owns is off-limits.
|
||||
if owner := ps.adapterOwner().ownedBy(name); owner != "" && owner != plugin {
|
||||
return "", fmt.Errorf("install_adapter: adapter %q is owned by plugin %q and cannot be replaced by %q",
|
||||
name, owner, plugin)
|
||||
}
|
||||
// Rules 1+2 (path safety + no shadowing built-ins) live in
|
||||
// InstallPluginAdapter so there is exactly one enforcement point.
|
||||
if err := ps.vm.InstallPluginAdapter(name, code); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Ownership is recorded under the name sources will actually reference. A
|
||||
// script may declare a name different from the registration key, and it is
|
||||
// the declared name that ends up in the registry — recording it under the
|
||||
// key would leave the adapter un-owned and therefore mutable by anyone.
|
||||
// Ownership is recorded under the name sources will actually reference. A
|
||||
// script may declare a name different from the registration key, and it is
|
||||
// the declared name that ends up in the registry — recording it under the
|
||||
// key would leave the adapter un-owned and therefore mutable by anyone.
|
||||
//
|
||||
// Both names are recorded: the registration key so a later update through
|
||||
// the same key still matches, and the declared name so a lookup by the name
|
||||
// a source references also matches. Both entries are cleared together on
|
||||
// remove.
|
||||
effective := ps.vm.EffectiveAdapterName(name)
|
||||
ps.adapterOwner().set(effective, plugin)
|
||||
if effective != name {
|
||||
ps.adapterOwner().set(name, plugin)
|
||||
}
|
||||
return effective, nil
|
||||
}
|
||||
|
||||
// UpdateAdapter replaces an adapter a plugin previously installed — the token
|
||||
// rotation path. The plugin gets a NEW pool; the old one is shut down, so an
|
||||
// in-flight request either uses the old or the new code, never a mixture.
|
||||
//
|
||||
// Only the owning plugin may update. Updating an operator-installed adapter is
|
||||
// refused: the operator's file on disk would then disagree with what the running
|
||||
// gateway executes, and the next restart would silently revert the "update".
|
||||
func (ps *Plugins) UpdateAdapter(plugin, name, code string) error {
|
||||
if plugin == "" {
|
||||
return fmt.Errorf("update_adapter: caller plugin is unknown")
|
||||
}
|
||||
owner := ps.adapterOwner().ownedBy(name)
|
||||
if owner == "" {
|
||||
return fmt.Errorf("update_adapter: adapter %q is operator-installed; "+
|
||||
"only adapters a plugin installed can be updated at runtime", name)
|
||||
}
|
||||
if owner != plugin {
|
||||
return fmt.Errorf("update_adapter: adapter %q is owned by plugin %q, not %q",
|
||||
name, owner, plugin)
|
||||
}
|
||||
if err := ps.vm.InstallPluginAdapter(name, code); err != nil {
|
||||
return err
|
||||
}
|
||||
// Ownership is unchanged: same plugin, new version.
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAdapter removes an adapter a plugin installed. Operator-installed
|
||||
// adapters are refused (same reasoning as UpdateAdapter).
|
||||
func (ps *Plugins) RemoveAdapter(plugin, name string) error {
|
||||
owner := ps.adapterOwner().ownedBy(name)
|
||||
if owner == "" {
|
||||
return fmt.Errorf("remove_adapter: adapter %q is operator-installed; "+
|
||||
"use DELETE /api/adapters/%s", name, name)
|
||||
}
|
||||
if owner != plugin {
|
||||
return fmt.Errorf("remove_adapter: adapter %q is owned by plugin %q, not %q",
|
||||
name, owner, plugin)
|
||||
}
|
||||
ps.vm.RemoveAdapter(name)
|
||||
ps.adapterOwner().clear(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginAdapters lists the adapters a plugin owns (its token-rotation surface).
|
||||
func (ps *Plugins) PluginAdapters(plugin string) []string {
|
||||
return ps.adapterOwner().listOwned(plugin)
|
||||
}
|
||||
|
||||
// AdapterOwnerOf reports the owning plugin of an adapter, or "" if operator-owned.
|
||||
func (ps *Plugins) AdapterOwnerOf(name string) string {
|
||||
return ps.adapterOwner().ownedBy(name)
|
||||
}
|
||||
Reference in New Issue
Block a user