Files
ModelRouter/internal/lua/plugin_adapter_bridge.go
JianFeeeee a0ab9ddda5 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 拒绝在脏工作区运行(设计如此),
故本提交后它才可运行。
2026-10-11 10:03:43 +08:00

188 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}