mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户指出「接口不全」——此前插件只有 __llmsproxy_adapters 三个手写闭包,
够不到内核。对照参考插件(dsh-our-free-model,13 个账号渠道)的真实能力,
缺的正是这两块:
插件自己驱动链接认证 → 存凭据 → 定时领额度 → 把源接进内核
## R4-c 内核动作面:__llmsproxy_kernel
18 个动作,全部映射到既有 Core 方法(不是新造能力):
只读 6:status / list_sources / list_models / list_adapters /
list_auto_rules / adapter_owner
写 8:reload / reset_health / probe_all / save_auto_rules /
save_key_auto / add_source / create_key / update_key
破坏性 4:remove_source / delete_key / remove_adapter / set_plugin_enabled
**add_source 是「插件注册自己的上游」的落点**——插件拿到凭据后可以自己把
端点接进网关,无需运维改 config.yaml。已端到端验证:插件调它 → 内核源列表
出现 → 模型进入可路由列表。
依赖方向:lua 不能 import core,所以接口定义在 lua、实现放 core
(kernel_actions.go),走 SetKernelAPI 注入——与 SetOutboundHosts 同一先例。
**凭据边界**:动作面**有意排除** list_keys/find_key。Core.ListKeys() 返回
明文网关密钥,交给第三方 Lua 等于交出整个部署的凭据面。有回归测试锁死
这条(TestKernelActionSurfaceExcludesPlaintextKeys)。list_sources 返回的
api_key 也强制掩码(只给 ***+后4位)。
**权限**(用户选「全开+设置页可配」):18 个动作全部可达,但**默认拒绝**,
授权落盘 .kernel-perms.json(独立文件,不并进 .access.json——后者是
map[string]string,塞进去会破坏旧二进制能读的格式)。danger 级不接受通配符。
插件可在源码 plugin.kernel={...} **申请**,设置页显示申请 vs 已授予。
**Go 侧 deadline**(用户选定):每个动作有超时预算,超时把 (nil,err) 还给
Lua。这可行的原因:Lua 只是等返回值,不占 Lua 栈执行权——与「Lua 死循环
不可中断」是两件事。
## R4-d 凭据保险库:__llmsproxy_secrets
此前插件存 token 只能写 plugin.state:0755 目录、明文、与计费累计量同文件、
会被 PUT /state 整体替换、会进 WebUI。现在独立文件:
<plugin_dir>/.secrets/<plugin>.json 文件 0600,目录 0700
临时文件**先 chmod 再 rename**,全程不存在宽权限窗口。读取身份在插件 boot
时绑定(不从 Lua 状态现读)⇒ 跨插件读不可行是机制而非约定。插件删除时清空
(DeleteAllSecrets + 启动 PurgeOrphanVaults)。
## 开发中被测试抓出的两个真实缺陷(我自己引入的)
写了 TestNoKernelActionDeadlocksFromLua 对**每个**动作断言「在插件 Lua 调用
(此时持有 p.mu)内不得死锁」,抓出:
1. **自死锁**:set_plugin_enabled → SetEnabled → p.mu.Lock(),而死锁发生在
调用者已持有的同一把锁上。
2. **更隐蔽的迟到副作用**:超时把 error 返回给插件后,动作仍在幕后执行完成——
插件以为失败,实际已生效。
修法:标注 Deferred 的动作在本次调用返回后异步执行,如实返回 accepted:true
而不是谎报成功。18 个动作全部复验无死锁,另有单独测试确认副作用真的落地
(只测「不死锁」的话,一个全空转的 dispatch 也能通过)。
测试 11 个(core 8 + lua 3):动作可用/密钥掩码/默认拒绝/授权计数/
插件注册源并变为可路由/授权落盘重启保留/未知动作名被拒/凭据面排除明文/
全动作无死锁/破坏性动作生效/Lua 侧桥接可用。
92 lines
2.7 KiB
Go
92 lines
2.7 KiB
Go
package lua
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
)
|
|
|
|
// Kernel action grants are OPERATOR policy, persisted separately from both
|
|
// plugin state and the endpoint/visibility overrides.
|
|
//
|
|
// Why a separate file rather than reusing .access.json:
|
|
//
|
|
// - .access.json is a flat map[string]string ("name" or "name#path" -> spec).
|
|
// Kernel grants are a list plus a boolean, so folding them in would either
|
|
// change the shape of a file an older binary still reads, or force an
|
|
// encoding that cannot round-trip. An upgrade must not corrupt a running
|
|
// deployment's restrictions.
|
|
// - The two answer different questions — "who may call this endpoint" vs
|
|
// "what may this plugin do to the kernel" — and an operator looking at one
|
|
// should not have to parse the other.
|
|
//
|
|
// Shape: { "<plugin>": { "actions": ["reload", ...], "all_read": true } }
|
|
|
|
const kernelPermsFileName = ".kernel-perms.json"
|
|
|
|
func (ps *Plugins) kernelPermsFile() string {
|
|
if ps.dir == "" {
|
|
return ""
|
|
}
|
|
return ps.dir + string(os.PathSeparator) + kernelPermsFileName
|
|
}
|
|
|
|
// LoadKernelPermissions reads the persisted grants. A missing file is the normal
|
|
// fresh-install state (nothing granted); a corrupt file is logged and ignored
|
|
// rather than fatal — losing a grant is recoverable, refusing to boot is not.
|
|
func (ps *Plugins) LoadKernelPermissions() error {
|
|
path := ps.kernelPermsFile()
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
b, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw := map[string]jsonPermission{}
|
|
if err := json.Unmarshal(b, &raw); err != nil {
|
|
return err
|
|
}
|
|
ps.mu.Lock()
|
|
defer ps.mu.Unlock()
|
|
if ps.kernelPerms == nil {
|
|
ps.kernelPerms = kernelPermissionSet{}
|
|
}
|
|
for name, jp := range raw {
|
|
ps.kernelPerms[name] = jp.unmarshal()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// saveKernelPermissionsLocked flushes the grant map. Caller holds ps.mu.
|
|
//
|
|
// Grants drop unknown action names on write. If an operator authorized an action
|
|
// that a later release removed, keeping the name would leave a stale entry that
|
|
// nothing can ever satisfy — and it would reappear in the UI as if it were live.
|
|
func (ps *Plugins) saveKernelPermissionsLocked() {
|
|
path := ps.kernelPermsFile()
|
|
if path == "" {
|
|
return
|
|
}
|
|
out := map[string]jsonPermission{}
|
|
for name, perm := range ps.kernelPerms {
|
|
valid := make([]string, 0, len(perm.Actions))
|
|
for _, a := range perm.Actions {
|
|
if _, ok := LookupKernelAction(a); ok {
|
|
valid = append(valid, a)
|
|
}
|
|
}
|
|
if len(valid) == 0 && !perm.AllRead {
|
|
continue // nothing granted: presence would be a lie
|
|
}
|
|
out[name] = KernelPermission{Actions: valid, AllRead: perm.AllRead}.marshal()
|
|
}
|
|
if len(out) == 0 {
|
|
_ = os.Remove(path)
|
|
return
|
|
}
|
|
writeJSONAtomic(path, out)
|
|
}
|