mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
迁移前,「外部插件拿不到 Selftest/Supervisor/Tracker」是 C ABI 表达能力的 **意外产物**——C 结构体不好传函数指针,这些能力自然到不了插件侧。那是运气 不是策略:任何人给 dispatch 加个 case 就能捅穿。 现在变成显式声明并强制,分三道闸: 1. **类型层**(proc_core.go,Part 6.2 已落地):procCore 用命名字段持有 内核 SDK 而非嵌入,未在收窄面写出的方法编译期就不存在。 2. **能力集**(新增 capability.go):54 个 plugin→kernel method 划入 11 个 capability 组,manifest 未声明的组被拒。 3. **RPC 边界**(corehandler.Handle 入口):被拒时返回**明确错误**而非 静默忽略。 第 3 条针对一类真实故障:C ABI 时代 case 23/24(事件订阅)是空实现, 返回成功但永远收不到事件(§1.3 的「给不了」而非「不给」),插件作者无从得知。 错误消息含四要素:哪个插件、哪个调用、缺什么能力、在哪声明。 ## 能力划分的两个判断 **粒度按能力域而非单 method**。逐 method 授权看似更精细,但插件作者要在 manifest 里列 60 个名字,且内核每加 method 所有 manifest 都得改。 **空声明 = 不受限,而非「只有 core」**。17 个存量插件的 plugin.json 都没有 capabilities 字段。若空声明当作最小权限,它们会全部失去 IO 注入、记忆读写 而**静默降级**——违反「外部插件零改动」的硬约束。收紧的路径是让插件显式 声明,而不是默默拒绝老插件。 ## core 与受限能力的边界 core(无需声明,始终可用):注册自身工具/阶段/通道/API、读写**自己的**配置、 共享段锁仲裁、握手、autoRestart 自述、setToolBlocks。没有这些插件无法工作。 受限(需声明):io / memory / doc_memory / knowledge / text_memory / llm / social / events / plugin_mgr / settings_cross。 settings 刻意拆成两级:读写自己的配置属 core(正常工作所需),读写**其他插件** 配置或**内核核心**配置属 settings_cross(能改别人/内核的行为)。 ## withheldCapabilities:让「不给」可见 10 项刻意不提供的内核内部机制列在表里并附理由。它们没有对应 method 常量—— 不是忘了加,是决定不加。列表存在本身就是「这是策略而非疏漏」的证据, 读代码的人能看到边界在哪,而不是从「protocol.go 里没有」这个负面事实去推断。 ## 测试 proc 包 10 项: - AllMethodsClassified:**最重要的一项**。漏登记的 method 会按 CapCore 放行, 等于绕过整套检查。新增 method 忘登记时当场报出。 - EmptyDeclarationIsUnrestricted / DeclaredSetRestrictsOthers / CoreAlwaysAllowed - SettingsScopeSeparation:自身配置 vs 跨插件配置的归属 - DeniedErrorIsActionable:错误消息四要素 - HandleEnforcesAtRPCBoundary:被拒的调用不进 switch - WithheldListIsDocumented:每项都有理由,且不被任何 method 暴露 - UnknownMethodFallsThrough:未知 method 报「未知」而非「权限被拒」, 否则作者会以为是漏声明能力 写这个测试时踩到自己的坑:第一版用子串匹配查 withheld 泄漏,"Tool" 匹配到 tool.register 和 io.setToolBlocks 误报——那两个是合法开放的(注册自己的工具)。 改成前缀 + unregister 关键字匹配,withheld 项也改名带 API 后缀以示区分。 internal/plugins 2 项接线验证: - RestrictedPluginStillLoads:只声明 io 的 weather 仍能加载并注册工具 (它在 Start 里读 Settings,属 core) - LegacyManifestUnrestricted:无 capabilities 字段的存量插件正常加载 真实 homed 实测: [plugin] weather-capped 声明能力: [io] [plugin] weather-capped: 经 proc 通道加载(子进程) registering tool: weather-capped_current / _forecast / _set_location 验证:go build ./... 通过;go test ./... 全仓无失败; go test -race ./internal/plugin/... 全绿;go vet 干净。 Ref: docs/zh/架构迁移评估.md §3.8、docs/zh/plugin-migration-plan.md Part 6.4
101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
//go:build linux || darwin
|
||
|
||
package plugins
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// manifest 声明的 capabilities 在真实内核加载路径上生效(Part 6.4)。
|
||
//
|
||
// capability_test.go 在 proc 包内验证判定逻辑;这里验证**接线**:
|
||
// manifest → readManifest → proc.New(caps...) → coreHandler.Handle 的强制。
|
||
|
||
// installPluginWithCaps 装插件并写入指定 capabilities 声明。
|
||
func installPluginWithCaps(t *testing.T, plgDir, name string, caps []string) {
|
||
t.Helper()
|
||
src := realPluginBinary(t, name)
|
||
|
||
dst := filepath.Join(plgDir, name)
|
||
if err := os.MkdirAll(dst, 0o755); err != nil {
|
||
t.Fatalf("建插件目录: %v", err)
|
||
}
|
||
data, err := os.ReadFile(src)
|
||
if err != nil {
|
||
t.Fatalf("读产物: %v", err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dst, "plugin.bin"), data, 0o755); err != nil {
|
||
t.Fatalf("写产物: %v", err)
|
||
}
|
||
|
||
capsJSON := ""
|
||
if caps != nil {
|
||
quoted := make([]string, len(caps))
|
||
for i, c := range caps {
|
||
quoted[i] = fmt.Sprintf("%q", c)
|
||
}
|
||
capsJSON = fmt.Sprintf(`,"capabilities":[%s]`, strings.Join(quoted, ","))
|
||
}
|
||
manifest := fmt.Sprintf(
|
||
`{"name":%q,"name_zh":%q,"name_en":%q,"version":"1.0.0","entry":"plugin.so"%s}`,
|
||
name, name, name, capsJSON)
|
||
if err := os.WriteFile(filepath.Join(dst, "plugin.json"), []byte(manifest), 0o644); err != nil {
|
||
t.Fatalf("写 manifest: %v", err)
|
||
}
|
||
}
|
||
|
||
// 声明了受限能力集的插件仍能正常加载并注册工具。
|
||
//
|
||
// core 能力(注册工具/阶段/通道 + 读写自己的配置)无需声明,
|
||
// 否则插件根本无法启动——weather 在 Start 里就要读 Settings。
|
||
func TestCapability_RestrictedPluginStillLoads(t *testing.T) {
|
||
env := setupIntegration(t)
|
||
defer env.cleanup()
|
||
|
||
plgDir := filepath.Join(env.tmpDir, "plugins")
|
||
// 只声明 io:weather 用到的 Settings 属 core,应放行
|
||
installPluginWithCaps(t, plgDir, "weather", []string{"io"})
|
||
|
||
if err := env.pluginReg.Load(plgDir); err != nil {
|
||
t.Fatalf("加载插件: %v", err)
|
||
}
|
||
if env.pluginReg.Get("weather") == nil {
|
||
t.Fatal("声明受限能力后插件应仍能加载(core 能力无需声明)")
|
||
}
|
||
|
||
// 工具注册也属 core
|
||
found := false
|
||
for _, def := range env.stageHost.GetToolDefs() {
|
||
if strings.Contains(def.Name, "weather") {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("受限插件仍应能注册工具(tool.register 属 core)")
|
||
}
|
||
}
|
||
|
||
// 未声明 capabilities 的插件不受限——存量插件向后兼容。
|
||
//
|
||
// 17 个存量插件的 plugin.json 都没有这个字段。若空声明当作最小权限,
|
||
// 它们会静默失去 IO 注入/记忆读写等能力,违反「外部插件零改动」。
|
||
func TestCapability_LegacyManifestUnrestricted(t *testing.T) {
|
||
env := setupIntegration(t)
|
||
defer env.cleanup()
|
||
|
||
plgDir := filepath.Join(env.tmpDir, "plugins")
|
||
installPluginWithCaps(t, plgDir, "weather", nil) // 无 capabilities 字段
|
||
|
||
if err := env.pluginReg.Load(plgDir); err != nil {
|
||
t.Fatalf("加载插件: %v", err)
|
||
}
|
||
if env.pluginReg.Get("weather") == nil {
|
||
t.Fatal("未声明 capabilities 的存量插件必须能正常加载")
|
||
}
|
||
}
|