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
152 lines
4.9 KiB
Go
152 lines
4.9 KiB
Go
//go:build linux || darwin
|
||
|
||
package plugin
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin/proc"
|
||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
)
|
||
|
||
// tryLoadProc 只做静态校验(供双通道探测与测试),不构造插件实体。
|
||
//
|
||
// 真正加载走 Registry.loadProc:子进程插件需要共享段 Host,
|
||
// 而 Host 必须是**全部 .bin 插件共用的那一个**,只能由 Registry 持有。
|
||
//
|
||
// 返回 nil,nil 表示目录中没有 plugin.bin(交由后续探测通道);
|
||
// 找到二进制但不可用时返回明确错误——不静默回退到 cabi。
|
||
func tryLoadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
_, err := validateProcBinary(dir, name)
|
||
return nil, err
|
||
}
|
||
|
||
// 子进程插件加载(plugin.bin)——外部插件多进程化的加载入口。
|
||
//
|
||
// 设计依据:docs/zh/架构迁移评估.md §3(stdio JSON-RPC 控制面 + shm 数据面 + eventfd 通知面)
|
||
// 实施计划:docs/zh/plugin-migration-plan.md Part 2/3
|
||
|
||
// validateProcBinary 校验 plugin.bin 是否存在且可执行。
|
||
// 返回 ("", nil) 表示该目录不是 proc 插件。
|
||
func validateProcBinary(dir, name string) (string, error) {
|
||
binPath := filepath.Join(dir, binEntry)
|
||
st, err := os.Stat(binPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return "", nil
|
||
}
|
||
return "", fmt.Errorf("proc plugin %s: 检查 %s: %w", name, binEntry, err)
|
||
}
|
||
if st.IsDir() {
|
||
return "", fmt.Errorf("proc plugin %s: %s 是目录,不是可执行文件", name, binEntry)
|
||
}
|
||
if st.Mode()&0o111 == 0 {
|
||
// 常见于经 zip/hmap 分发丢失权限位——给出可直接执行的修复指令
|
||
return "", fmt.Errorf("proc plugin %s: %s 缺少可执行权限(chmod +x %s)",
|
||
name, binEntry, binPath)
|
||
}
|
||
return binPath, nil
|
||
}
|
||
|
||
// loadProc 构造子进程插件实体(不 spawn)。
|
||
//
|
||
// 共享段 Host 在此惰性创建:**全部 .bin 插件共用一块段**。
|
||
// 若每插件一段,多插件同阶段并发时会退化成副本模型,
|
||
// lost update 原样复现(§8.4 实测 35.8~36.8%)。
|
||
func (r *Registry) loadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
binPath, err := validateProcBinary(dir, name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if binPath == "" {
|
||
return nil, nil
|
||
}
|
||
|
||
host, err := r.ensureProcHost()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("proc plugin %s: %w", name, err)
|
||
}
|
||
|
||
// manifest 声明的能力集(§3.8 权限梯度)。
|
||
// 无 manifest 或未声明 capabilities 时不限制,保存存量插件行为。
|
||
var caps []string
|
||
if mft := readManifest(dir); mft != nil {
|
||
caps = mft.Capabilities
|
||
if len(caps) > 0 {
|
||
log.Printf("[plugin] %s 声明能力: %v", name, caps)
|
||
}
|
||
}
|
||
|
||
return procPluginAdapter{
|
||
Plugin: proc.New(name, binPath, dir, config, host, r.onProcCrash, caps...),
|
||
}, nil
|
||
}
|
||
|
||
// ensureProcHost 惰性创建共享段 Host(全进程唯一)。
|
||
func (r *Registry) ensureProcHost() (*proc.Host, error) {
|
||
r.procHostMu.Lock()
|
||
defer r.procHostMu.Unlock()
|
||
if r.procHost != nil {
|
||
return r.procHost, nil
|
||
}
|
||
host, err := proc.NewHost()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
r.procHost = host
|
||
|
||
// 事件环适配层:Bus 发布 → 写 EvtRing slot → eventfd 通知子进程
|
||
if r.evBus != nil {
|
||
er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), r.evBus)
|
||
host.SetEvtSubscriber(er)
|
||
log.Printf("[plugin] 事件环已创建(Bus → EvtRing → eventfd)")
|
||
}
|
||
|
||
log.Printf("[plugin] 共享段已创建(全部子进程插件共用一块,%d KB)", host.ShmSize()/1024)
|
||
return host, nil
|
||
}
|
||
|
||
// closeProcHost 释放共享段(仅在内核关停时调用)。
|
||
func (r *Registry) closeProcHost() {
|
||
r.procHostMu.Lock()
|
||
defer r.procHostMu.Unlock()
|
||
if r.procHost == nil {
|
||
return
|
||
}
|
||
if err := r.procHost.Close(); err != nil {
|
||
log.Printf("[plugin] 关闭共享段: %v", err)
|
||
}
|
||
r.procHost = nil
|
||
}
|
||
|
||
// onProcCrash 在子进程插件异常退出时回调。
|
||
//
|
||
// **崩溃隔离**:子进程死亡只影响自己,homed 继续服务——对比 C ABI 下
|
||
// 插件 panic 直接带崩整个进程(§1.2,现网已发生)。
|
||
//
|
||
// 崩溃计数/冷却/自愈复用既有 plugin_health(§2.3),本函数只负责把
|
||
// 进程退出这一事实转成事件通知;具体重载策略由 agent 侧决定。
|
||
func (r *Registry) onProcCrash(name string, err error) {
|
||
log.Printf("[plugin] 子进程插件 %s 异常退出: %v(homed 未受影响)", name, err)
|
||
if r.evBus == nil {
|
||
return
|
||
}
|
||
// 不在此处直接重载:重载需要 registry 锁,而本回调可能在
|
||
// 持锁路径的 goroutine 中触发,直接调用会死锁。
|
||
r.evBus.Publish(&events.Event{
|
||
Type: events.EventSystem,
|
||
Source: "plugin",
|
||
Payload: map[string]interface{}{
|
||
"event": "plugin_crashed",
|
||
"plugin": name,
|
||
"error": err.Error(),
|
||
},
|
||
Timestamp: time.Now().Unix(),
|
||
})
|
||
}
|