// 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" "strings" "sync" golua "github.com/aarzilli/golua/lua" ) // 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") } // A name conflict used to be a hard rejection (owned by another plugin, or // shadows a built-in). The user now wants it SUSPENDED for operator // arbitration instead of refused outright: the incumbent keeps serving, the // candidate waits in the conflicts list, and the operator picks a winner in // the WebUI. The install still returns a (typed) error so the caller knows it // did not take effect — but it is ErrAdapterConflictPending, not a dead end. incumbent := "" if owner := ps.adapterOwner().ownedBy(name); owner != "" && owner != plugin { incumbent = "plugin:" + owner } else if containsNameFold(ps.vm.bundledNames(), name) { incumbent = "operator (built-in)" } if incumbent != "" { if ps.recordAdapterConflict(name, incumbent, plugin, code) { // The operator already chose THIS candidate in a prior session. Install // for real via LoadAdapterSource, which bypasses the reserved-name gate // — that explicit operator override is exactly what the arbitration is // FOR, and re-suspending it on every restart would be a loop. if err := ps.vm.LoadAdapterSource(name, code); err != nil { return "", err } return ps.finishInstall(plugin, name), nil } return "", &ErrAdapterConflictPending{Name: name, Incumbent: incumbent} } // No conflict: normal install path. Rules 1+2 (path safety + no shadowing // built-ins) live in InstallPluginAdapter so there is exactly one // enforcement point. (The shadow check there is now unreachable for a // genuine built-in — it is caught above and suspended — but kept as the // single authority for path safety and any future reserved set.) if err := ps.vm.InstallPluginAdapter(name, code); err != nil { return "", err } return ps.finishInstall(plugin, name), nil } // finishInstall records adapter ownership under BOTH the registration key and // the declared name (a script may declare a name different from the key, and it // is the declared name sources reference — recording only the key would leave // the adapter un-owned and therefore mutable by anyone). Both entries are // cleared together on remove. func (ps *Plugins) finishInstall(plugin, name string) string { effective := ps.vm.EffectiveAdapterName(name) ps.adapterOwner().set(effective, plugin) if effective != name { ps.adapterOwner().set(name, plugin) } return effective } // containsNameFold reports whether name is in names (case-insensitive, matching // ValidateAdapterName's reserved-name comparison). func containsNameFold(names []string, name string) bool { for _, n := range names { if strings.EqualFold(n, name) { return true } } return false } // 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) } // ---- Lua 绑定(R4:插件在运行时改自己的适配器)---- // adapterBridgeGlobal 暴露给插件 Lua 的适配器管理表。 // // 用一张表而不是三个顶层全局,是为了少占插件可见的全局命名空间。表用 // buildJSONTable 的同一套写法(NewTable + PushGoClosure + SetField), // 该写法在 json 表上已被长期验证。 const adapterBridgeGlobal = "__llmsproxy_adapters" // registerAdapterBridge 把适配器管理表装进插件的 Lua 状态。 // // name 是**该插件自己的启动名**,随每次调用一起交给 Go 侧桥接 —— 归属校验 // 因此来自启动时的固定身份,而不是从 Lua 状态里现读(那样一个分片或任何持有 // 已编译脚本的调用者就能冒充别的插件)。 func registerAdapterBridge(L *golua.State, ps *Plugins, name string) { L.NewTable() install := luaInstallAdapter(L, ps, name) L.PushGoClosure(install) L.SetField(-2, "install") update := luaUpdateAdapter(L, ps, name) L.PushGoClosure(update) L.SetField(-2, "update") remove := luaRemoveAdapter(L, ps, name) L.PushGoClosure(remove) L.SetField(-2, "remove") L.SetGlobal(adapterBridgeGlobal) } func luaInstallAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction { return func(L *golua.State) int { adapterName := L.ToString(1) code := L.ToString(2) effective, err := ps.InstallAdapter(name, adapterName, code) if err != nil { L.PushNil() L.PushString(err.Error()) return 2 } L.PushString(effective) L.PushNil() return 2 } } func luaUpdateAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction { return func(L *golua.State) int { adapterName := L.ToString(1) code := L.ToString(2) if err := ps.UpdateAdapter(name, adapterName, code); err != nil { L.PushNil() L.PushString(err.Error()) return 2 } L.PushBoolean(true) L.PushNil() return 2 } } func luaRemoveAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction { return func(L *golua.State) int { adapterName := L.ToString(1) if err := ps.RemoveAdapter(name, adapterName); err != nil { L.PushNil() L.PushString(err.Error()) return 2 } L.PushBoolean(true) L.PushNil() return 2 } }