feat: 设备鉴权迁移至客户端 + 插件卸载保护

安全修复(客户端鉴权):
- remotedevice 服务端移除授权状态存储(authorized map/SetAuthorized/handleDeviceAuth)
- DeviceMeta.Authorized 改为设备 hello 自报,服务端仅透传展示
- device_ctl_* 工具移除服务端授权检查,无条件转发,设备端自行决定是否执行
- 共享设备桥库 Bridge 新增本地 authorized 状态,未授权收到 cmd 直接拒绝
- waiter: --device-authorized / device_authorized 配置控制本地授权
- GUI: 授权存 gui-prefs 本地文件;设备页仅本机可切换开关
- webui /device/auth 旧路径返回 410 Gone
- 根因:agent 可经 config_set 篡改服务端授权配置自行授权设备

插件管理强化:
- 内置插件禁止卸载(IsBuiltinPlugin + 409),外部插件卸载即时生效
- 卸载不存在插件返回 404;移除误导性 reload_required 提示
- webui 插件路由:名称白名单校验防路径穿越、保留字路径保护
This commit is contained in:
JianFeeeee
2026-08-24 19:26:11 +08:00
parent 5163ce51a7
commit ba5785036a
21 changed files with 616 additions and 383 deletions

View File

@ -39,18 +39,18 @@ func platformBinary() (zipName, canonicalName string) {
// validBinaries 是 .hmap 中所有可识别的文件入口(平台二进制或脚本)。
var validBinaries = map[string]bool{
"plugin.so": true,
"plugin.so": true,
"plugin.dylib": true,
"plugin.dll": true,
"main.lua": true,
"SKILL.md": true,
"plugin.dll": true,
"main.lua": true,
"SKILL.md": true,
}
// platformBinaries 是平台特定的二进制bundle 模式下仅当前平台的被解压。
var platformBinaries = map[string]bool{
"plugin.so": true,
"plugin.so": true,
"plugin.dylib": true,
"plugin.dll": true,
"plugin.dll": true,
}
var downloadClient = &http.Client{
@ -312,7 +312,16 @@ func (p *Plugin) handlePluginByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
result, err := p.removePlugin(name)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
// 内置插件禁卸 → 409 Conflict不存在 → 404其余删除失败 → 500
msg := err.Error()
switch {
case strings.Contains(msg, "built-in plugin"):
writeJSON(w, http.StatusConflict, map[string]interface{}{"error": msg, "name": name, "plugin_type": "builtin"})
case strings.Contains(msg, "not found"):
writeJSON(w, http.StatusNotFound, map[string]interface{}{"error": msg, "name": name})
default:
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{"error": msg, "name": name})
}
return
}
writeJSON(w, http.StatusOK, result)
@ -451,9 +460,14 @@ func (p *Plugin) listPlugins() (interface{}, error) {
}
func (p *Plugin) removePlugin(name string) (interface{}, error) {
// 内置插件只能禁用不能卸载:目录下无产物,且从注册表删除会破坏内核依赖。
if p.sdk != nil && p.sdk.PluginMgr() != nil && p.sdk.PluginMgr().IsBuiltinPlugin(name) {
return nil, fmt.Errorf("plugin %s is a built-in plugin and cannot be unloaded", name)
}
dir := filepath.Join(p.pluginDir, name)
if _, err := os.Stat(dir); os.IsNotExist(err) {
return map[string]interface{}{"error": "plugin not found", "name": name}, nil
return nil, fmt.Errorf("plugin %s not found", name)
}
// 先经内核卸载停止插件stop handlers + Stop并执行插件注册的 onRemove 回调
@ -464,7 +478,7 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
}
if err := os.RemoveAll(dir); err != nil {
return map[string]interface{}{"error": err.Error()}, nil
return nil, fmt.Errorf("remove plugin dir: %w", err)
}
// 同步清理禁用表
@ -474,10 +488,11 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
}
}
// 卸载已即时生效(停止+注册表移除+目录删除),无需 reload
return map[string]interface{}{
"status": "removed",
"name": name,
"action": "reload_required",
"status": "removed",
"name": name,
"reload_required": false,
}, nil
}